実際の開発では、1つのアプリケーションが複数のプログラムで構成されることがあり、それらのコンポーネント間のデータ連携が重要になります。どのようにすれば高速かつ効率的にデータをやり取りできるでしょうか。サーバーを跨ぐプロセス間通信では、Remoting、WCF、gRPCなどのリモートプロシージャコール(RPC)技術を使用できます。ただし、これらはネットワークカードを介した転送が必要であり、データ変換やネットワーク転送によるパフォーマンスの低下が生じます。同じサーバー上のプロセス間であれば、RPCを使用するよりも最適な方法があります。ネットワークを介さずにプロセス間でデータをやり取りする方法、それが「共有メモリ」です。今回は、簡単なサンプルを通して、プロセス間で共有メモリを使用したデータ連携の基礎を解説します。学習や共有を目的としており、不十分な点があればご指摘ください。

共有メモリとは?
オペレーティングシステムでは、各プロセスに独立したメモリ領域が割り当てられ、プログラムの実行やデータの保存に使用されます。プロセス間のメモリは互いに独立しており、干渉しないため、プログラムの安定した動作が保証されます。しかし、このプロセスメモリ保護機構はデータの安全性とプログラムの安定性に大きく貢献する一方で、相互に通信する必要があるプロセス間では越え難い壁となります。幸い、オペレーティングシステムはこの状況を考慮しており、それが「共有メモリ」です。共有メモリ(Shared Memory)はプロセス間通信(IPC) の仕組みの1つで、複数のプロセスが同じ物理メモリを共有することで、データ交換の効率を向上させます。パイプやメッセージキューなどの他のIPC方式と比較して、共有メモリは速度が速く、オーバーヘッドが低いという利点があります。これは、データが直接メモリに格納され、カーネルを介したデータコピーが不要だからです。
.NETにおける共有メモリ
.NETプラットフォームでは、共有メモリはMemoryMappedFileを使用して実現されます。メモリマップドファイルを使用すると、アドレス空間の領域を確保し、その物理ストレージをこのメモリ空間にマッピングして操作できます。物理ストレージはファイル管理であり、メモリマップドファイルはOSレベルのメモリ管理です。メモリマップドファイル技術に関連する主なポイントは以下の通りです。
- 共有メモリの作成:メモリマップドファイルの作成方法は2つあります。1つは直接作成する方法で、
MemoryMappedFile.CreateNewメソッドを使用します。もう1つは、既存のファイルから作成する方法で、MemoryMappedFile.CreateFromFileメソッドを使用します。 - 共有メモリアクセサ:.NETでは、
MemoryMappedViewAccessorを使用して共有メモリにアクセスします。これはMemoryMappedFileインスタンスのCreateViewAccessorメソッドで作成します。 - 読み書き方式:共有メモリはバイト配列としてデータを格納します。アクセサの
ReadArrayおよびWriteArrayメソッドを使用してバイト配列の読み書きを行います。
メモリマップドファイルの作成
メモリマップドファイルには2つの作成方法があります。
1つ目は直接作成する方法で、MemoryMappedFileの静的メソッドCreateNewを使用します。以下に示します。

2つ目は、MemoryMappedFileの静的メソッドCreateFromFileを使用する方法で、既存のファイルやファイルストリームを利用して作成します。以下に示します。

既存のメモリマップドファイルを開くには、MemoryMappedFileの静的メソッドOpenExistingを使用します。以下に示します。

メモリマップドファイルを作成または開くには、MemoryMappedFileの静的メソッドCreateOrOpenを使用します。以下に示します。

メモリマップドファイルアクセサ
メモリマップドファイルアクセサは、主に共有メモリの操作に使用され、MemoryMappedFileインスタンスのCreateViewAccessorメソッドで作成します。以下に示します。

メモリマップドファイルのリソース解放
MemoryMappedFileはIDisposableインターフェースを実装しているため、Disposeメソッドを直接呼び出します。
共有メモリのアプリケーション手順
このサンプルでは、2つのWinForm実行可能プログラムで構成されており、それぞれ共有メモリの読み取り/書き込みを行います。実行時には両方のプログラムが同時に動作します。以下に示します。

主に、固定フォーマットの構造体データの読み書きと、可変長データの読み書きを紹介します。
メモリマップドファイルオブジェクトの作成
このサンプルでは、共有メモリの書き込み時にメモリマップドファイルを作成し、読み取り時に開きます。以下に示します。
/// <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();
}
可変長バイトの読み書き
可変長のバイト配列による読み取りでは、ユーザーが開いた画像を例に、画像のパスと画像コンテンツをバイト配列として共有メモリを介してデータ交換します。共有メモリ内の格納形式は以下の図のようになります。

エンティティモデルImageData
まず、エンティティモデルImageDataを作成します。その主な機能は、画像オブジェクトBitmapとバイト配列の間で変換を行うことです。以下に示します。
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();
}
}
}
共有メモリのバイト読み書き
可変長の共有メモリのバイト読み書きは以下の通りです。
/// <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オブジェクトをカプセル化し、バイト配列に変換して共有メモリに書き込みます。以下に示します。
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;
}
これで、咻咻二回で、一方のプログラムでユーザーが選択した画像が共有メモリを通じて別のアプリケーションに渡されます。不思議でしょう?驚きでしょう?
固定長コンテンツの読み書き
実際のアプリケーションでは、共有メモリは値型の構造体や参照型のバイト配列のデータ送信をサポートします。構造体内のプロパティの順序は、メモリ上の順序と一致します。
エンティティモデルの定義
まず、構造体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.AllocHGlobalでメモリを確保し、Marshal.StructureToPtrで構造体を確保したメモリに格納し、ポインタを先頭に設定します。次に、Marshal.Copyでポインタが指すメモリをバイト配列にコピーし、共有メモリに書き込みます。最後に、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());
}
サンプルデモ
まず、可変長の画像をプロセス間で交換するデモです。以下に示します。

固定長の構造体型をプロセス間で交換するデモです。以下に示します。

以上が「プロセス間の高速データ交換ソリューションのご紹介」の全内容です。きっかけとして共有し、共に学び、成長していきたいと思います。
