在實際開發中,一款應用可能有多個應用程式組成,那這款應用各個組成部分之間的資料交互就成了關鍵,如何才能快速高效的進行資料交互呢?如果是跨伺服器的行程交互,可以採用Remoting,WCF,GRPC等遠端程序呼叫技術(RPC),這種方式會經過網卡進行網路傳輸,存在一定的資料轉換及網路傳輸等效能消耗。如果是同一台伺服器的行程間資料交互,也採用這種遠端程序呼叫技術,則不是最優方案。那如何才能繞過網路來進行跨行程資料交互呢?答案就是「共享記憶體」,今天我們就以一個簡單的小例子,簡述行程間如何透過共享記憶體進行資料交互的應用,僅供學習分享使用,如有不足之處,還請指正。

什麼是共享記憶體?
在作業系統中,系統會為每一個行程分配一塊獨立的記憶體,以供行程執行程式和儲存資料,各個行程間的記憶體相互獨立,互不干擾,以保證程式的穩定有序的執行。雖然這種行程的記憶體保護機制在很大程度上保證了資料安全和程式穩定,但在需要進行交互的行程之間,也形成了難以逾越的壁壘。幸好作業系統也考慮到了這種情況,那就是共享記憶體。共享記憶體(Shared Memory)是一種 行程間通訊(IPC) 機制,允許多個行程共享同一塊實體記憶體,從而提高資料交換效率。相比其他 IPC 方式(如管道、訊息佇列等),共享記憶體具有 速度快、低開銷 的優勢,因為資料直接儲存在記憶體中,而無需透過核心進行資料拷貝。
.NET中的共享記憶體
在.NET平台,共享記憶體透過MemoryMappedFile來實現,記憶體映射檔案允許你保留一塊位址空間,然後將該實體儲存映射到這塊記憶體空間中進行操作。實體儲存是檔案管理,而記憶體映射檔案是**作業系統級記憶體管理。**記憶體映射檔案技術主要涉及的知識點如下所示:
- 建立共享記憶體:記憶體映射檔案的建立有兩種方式,一種是直接建立,透過MemoryMappedFile.CreateNew方法來實現;一種是透過已經存在的檔案進行建立,它透過呼叫MemoryMappedFile.CreateFromFile來實現。
- 共享記憶體存取器:在.NET中,透過MemoryMappedViewAccessor來存取共享記憶體,它透過MemoryMappedFile的執行個體物件的CreateViewAccessor來建立。
- 讀取寫入方式:共享記憶體中以Byte陣列的方式儲存資料,可以透過存取器的ReadArray和WriteArray來讀取和寫入位元組陣列。
記憶體映射檔案建立
記憶體映射檔案有兩種建立形式:
第1種是直接建立,它透過MemoryMappedFile提供的靜態方法CreateNew來建立,如下所示:

第2種是透過MemoryMappedFile提供的靜態方法CreateFromFile來建立,它是利用已經存在的檔案或檔案串流進行建立,如下所示:

開啟已存在的記憶體映射檔案,它透過MemoryMappedFile提供的靜態方法OpenExist來實現,如下所示:

建立或開啟記憶體映射檔案,它透過MemoryMappedFile提供的靜態方法CreateOrOpen來實現,如下所示:

記憶體映射檔案存取器
記憶體映射檔案存取器,主要用於操作共享記憶體,它透過MemoryMappedFile執行個體物件的CreateViewAccessor來實現,如下所示:

記憶體映射檔案資源釋放
MemoryMappedFile實作了IDisposable介面,直接呼叫它的Dispose方法即可。
共享記憶體應用步驟
在本實例中,主要有兩個WinForm可執行程式組成,分別進行讀共享記憶體/寫共享記憶體,執行時兩個程式同時執行,如下所示:

主要介紹固定格式的Struct資料讀寫共享記憶體,和不定長的資料讀寫共享記憶體。
記憶體映射檔案物件建立
本實例在寫共享記憶體時建立記憶體映射檔案,讀共享記憶體時開啟記憶體映射檔案,如下所示:
/// <summary>
/// 建立或開啟共享記憶體
/// </summary>
public void CreateOrOpenSharedMemory()
{
this.memoryMapped = MemoryMappedFile.CreateOrOpen(this.MapName, this.capacity, MemoryMappedFileAccess.ReadWriteExecute, MemoryMappedFileOptions.None, HandleInheritability.Inheritable);
this.memoryAccessor = this.memoryMapped.CreateViewAccessor();
}
/// <summary>
/// 從檔案建立共享記憶體
/// </summary>
public void CreateFromFileShareMemory()
{
this.memoryMapped = MemoryMappedFile.CreateFromFile(new FileStream(@"", FileMode.Create), this.MapName, this.capacity, MemoryMappedFileAccess.ReadWriteExecute, HandleInheritability.Inheritable, true);
this.memoryAccessor = this.memoryMapped.CreateViewAccessor();
}
/// <summary>
/// 開啟已存在的共享記憶體
/// </summary>
public void OpenShareMemory()
{
this.memoryMapped = MemoryMappedFile.OpenExisting(this.MapName);
this.memoryAccessor = this.memoryMapped.CreateViewAccessor();
}
不定長度位元組讀寫
不定長度的位元組陣列方式讀取,以使用者開啟的圖片為例,將圖片路徑和圖片內容以Byte陣列的形式透過共享記憶體進行資料交換。它們在共享記憶體中的儲存格式如下圖所示:

實體模型ImageData
首先建立實體模型ImageData,它的主要功能是將圖片物件Bitmap和Byte陣列之間進行轉換,如下所示:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Okcoder.ShareMemory.Common
{
public class ImageData
{
public string ImageFullPath { get; set; }
public Bitmap ImageContent { get; set; }
/// <summary>
/// 從物件轉換成陣列
/// </summary>
/// <returns></returns>
public byte[] ImageToBytes()
{
var byteFullPath = Encoding.UTF8.GetBytes(this.ImageFullPath);
MemoryStream stream = new MemoryStream();
int lenFullPath = byteFullPath.Length;
byte[] byteFullPathLen = BitConverter.GetBytes(lenFullPath);
ImageContent.Save(stream, ImageContent.RawFormat);
var byteImageContent = stream.ToArray();
int lenImageContent = byteImageContent.Length;
byte[] byteImageContentLen = BitConverter.GetBytes(lenImageContent);
byte[] total = new byte[4 + lenFullPath + 4 + lenImageContent];
byteFullPathLen.CopyTo(total, 0);
byteFullPath.CopyTo(total, 4);
byteImageContentLen.CopyTo(total, 4 + lenFullPath);
byteImageContent.CopyTo(total, 4 + lenFullPath + 4);
stream.Close();
stream.Dispose();
return total;
}
/// <summary>
/// 從物件轉換成物件
/// </summary>
/// <param name="bytes"></param>
public void BytesToImage(byte[] bytes)
{
int lenFullPathLen = BitConverter.ToInt32(bytes, 0);
var byteFullPath = new byte[lenFullPathLen];
bytes.Skip(4).Take(lenFullPathLen).ToArray().CopyTo(byteFullPath, 0);
this.ImageFullPath = Encoding.UTF8.GetString(byteFullPath);
int lenImageContent = BitConverter.ToInt32(bytes, 4 + lenFullPathLen);
var byteImageContent = new byte[lenImageContent];
bytes.Skip(4 +lenFullPathLen+4).Take(lenImageContent).ToArray().CopyTo(byteImageContent,0);
MemoryStream stream = new MemoryStream(byteImageContent);
this.ImageContent = (Bitmap)Image.FromStream(stream);
stream.Close();
stream.Dispose();
}
}
}
共享記憶體位元組讀寫
不定長度的共享記憶體的Byte讀/寫,如下所示:
/// <summary>
/// 讀取位元組
/// </summary>
/// <returns></returns>
public byte[] ReadMemoryWithBytes()
{
byte[] bytes = new byte[this.capacity];
this.memoryAccessor.ReadArray<byte>(0, bytes, 0, bytes.Length);
return bytes;
}
/// <summary>
/// 寫入位元組
/// </summary>
/// <param name="bytes"></param>
public void WriteMemoryWithBytes(byte[] bytes)
{
this.memoryAccessor.WriteArray<byte>(0, bytes, 0, bytes.Length);
}
其中capacity為記憶體映射檔案的預設容量,大小為10M。
寫共享記憶體呼叫
首先在Okcoder.ShareMemory.Writer專案頁面,使用者選擇一張圖片,並顯示在UI頁面上,如下所示:
private void btnBrowser_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Title = "請選擇圖片";
openFileDialog.Filter = "PNG圖片|*.png|JPG圖片|*.jpg";
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
string fileName = openFileDialog.FileName;
this.pictureBox1.Image = Bitmap.FromFile(fileName);
this.pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
this.txtImagePath.Text = fileName;
}
}
然後點選「咻一下」按鈕封裝ImageData物件,並轉換成Byte陣列,然後寫入共享記憶體 ,如下所示:
private void btnWriteMemory_Click(object sender, EventArgs e)
{
//定義ImageData並轉換成位元組陣列
ImageData imageData = new ImageData();
imageData.ImageFullPath = this.txtImagePath.Text;
imageData.ImageContent = (Bitmap)this.pictureBox1.Image;
byte[] bytes = imageData.ImageToBytes();
//定義共享記憶體幫助物件並開啟共享記憶體
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.CreateOrOpenSharedMemory();
//以位元組方式寫入共享記憶體
helper.WriteMemoryWithBytes(bytes);
}
讀共享記憶體呼叫
在Okcoder.ShareMemory.Reader專案頁面,使用者點選「咻一下」按鈕,讀取共享記憶體,並轉換內ImageData,然後顯示在UI頁面上,如下所示:
private void btnRead_Click(object sender, EventArgs e)
{
//定義共享記憶體幫助物件並開啟共享記憶體
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.OpenShareMemory();
//以位元組方式讀取
byte[] bytes = helper.ReadMemoryWithBytes();
ImageData imageData = new ImageData();
//轉換位元組到ImageData物件
imageData.BytesToImage(bytes);
//頁面指派
this.txtImagePath.Text = imageData.ImageFullPath;
this.pictureBox1.Image = imageData.ImageContent;
this.pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
}
這樣透過咻咻兩下,就可以將一個程式使用者選擇的圖片,透過共享記憶體,傳遞到另一個應用程式。神不神奇,意不意外!
固定長度內容讀寫
在實際應用中,共享記憶體支援值型別的結構體,參考型別的Byte陣列的資料床底,結構體中的屬性排序,就是它在記憶體中的順序。
定義實體模型
首先定義結構體TestData,為了能夠讓結構體在指標之間進行轉換,必須透過StructLayout特性將結構體宣告為可序列化,如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Okcoder.ShareMemory.Common
{
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
public struct TestData
{
/// <summary>
/// Id,長度為4個位元組
/// </summary>
public int Id;
/// <summary>
/// 年齡長度為4個位元組
/// </summary>
public int Age;
/// <summary>
/// 分數 10個元素
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x0A)]
public int[] Scores;
public TestData()
{
this.Id = 0;
this.Age = 0;
this.Scores = new int[10];
}
public override string ToString()
{
return $"Id={this.Id},Age={this.Age},Scores={string.Join(",",this.Scores)}";
}
}
}
寫結構體類型
首先透過Marshal申請記憶體空間,並透過Marshal的StructureToPtr方法將結構體儲存到申請的記憶體中,並指標指向開始位置,然後再透過Marshal的Copy方法將指標指向的記憶體複製到Byte陣列中,然後寫入到共享記憶體,最後透過Marshal的FreeHGlobal釋放申請的指標,如下所示:
/// <summary>
/// 寫入結構體
/// </summary>
/// <param name="data"></param>
public void WriteMemoryWithStruct(TestData data)
{
//獲取結構體的長度
int len = Marshal.SizeOf(typeof(TestData));
byte[] bytes = new byte[len];
//申請記憶體並取得指標
IntPtr p = Marshal.AllocHGlobal(len);
//結構體寫入記憶體
Marshal.StructureToPtr(data, p, false);
//將記憶體複製到陣列
Marshal.Copy(p, bytes, 0, len);
//寫入陣列共享到記憶體
this.memoryAccessor.WriteArray<byte>(0, bytes, 0, bytes.Length);
//釋放記憶體
Marshal.FreeHGlobal(p);
//釋放指標
p = IntPtr.Zero;
}
讀取結構體類型
首先透過Marshal的AllocHGlobal方法申請記憶體空間,並將共享記憶體的位元組陣列讀取出來並複製到申請的記憶體中,並指標指向開始位置,然後再透過Marshal的指標轉結構體方法PtrToStructure,轉換成結構體物件,最後透過Marshal的FreeHGlobal釋放申請的指標,如下所示:
/// <summary>
/// 讀取結構體
/// </summary>
/// <returns></returns>
public TestData ReadMemoryWithStruct()
{
//獲取結構體類型的長度
int len = Marshal.SizeOf(typeof(TestData));
byte[] bytes = new byte[len];
//申請記憶體
IntPtr p = Marshal.AllocHGlobal(len);
//從共享記憶體讀取資料
this.memoryAccessor.ReadArray<byte>(0, bytes, 0, bytes.Length);
//將位元組陣列複製到指標
Marshal.Copy(bytes, 0, p, len);
//指標轉換到結構體
TestData data = (TestData)Marshal.PtrToStructure(p, typeof(TestData));
//釋放記憶體
Marshal.FreeHGlobal(p);
//釋放指標
p = IntPtr.Zero;
return data;
}
寫共享記憶體呼叫
在Okcoder.ShareMemory.Writer專案頁面,使用者點選Struct按鈕,然後封裝TestData執行個體物件,然後寫入共享記憶體,如下所示:
private void btnWriteStruct_Click(object sender, EventArgs e)
{
//定義TestData並指派
TestData testData = new TestData();
testData.Id = 100;
testData.Age = 20;
for (int i = 0; i < 10; i++)
{
testData.Scores[i] = i + 60;
}
//定義共享記憶體幫助物件並開啟共享記憶體
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.CreateOrOpenSharedMemory();
//以結構體的形式寫入共享記憶體
helper.WriteMemoryWithStruct(testData);
}
讀共享記憶體呼叫
在Okcoder.ShareMemory.Reader專案頁面,使用者點選Struct按鈕,從共享記憶體獲取結構體並彈框顯示,如下所示:
private void btnReadStruct_Click(object sender, EventArgs e)
{
//定義共享記憶體幫助物件並開啟共享記憶體
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.OpenShareMemory();
//讀取結構體
TestData testData = helper.ReadMemoryWithStruct();
MessageBox.Show(testData.ToString());
}
實例示範
首先是不定長度的圖片在行程間交換資料,如下所示:

固定長度的結構體類型在行程間交換資料,如下所示:

以上就是《推薦一款行程間高速交換資料的解決方案》的全部內容,旨在拋磚引玉,一起學習,共同進步!
