C#共有メモリの使い方を完全解説|MemoryMappedFileでプロセス間通信する実装例と注意点
はじめに
C#で複数のプロセス間にデータを共有したい場合、代表的な選択肢のひとつが共有メモリです。共有メモリを使うと、あるプロセスが書き込んだデータを、別のプロセスが同じメモリ領域から読み取れるようになります。
C#では、共有メモリの実装にSystem.IO.MemoryMappedFiles.MemoryMappedFileを使うのが一般的です。MemoryMappedFileは、ファイルまたはシステムメモリ上の領域をプロセスのアドレス空間にマッピングし、複数プロセスから同じ領域へアクセスできる仕組みを提供します。Microsoftの公式ドキュメントでも、メモリマップトファイルは複数プロセス間で共有でき、共通の名前を使って同じメモリマップトファイルへマップできると説明されています。
この記事では、C#共有メモリの基本概念から、MemoryMappedFileを使ったプロセス間通信の実装例、MutexやEventWaitHandleによる同期処理、よくあるエラー、安全に使うための注意点までを実践的に解説します。
1. C#共有メモリとは?プロセス間通信で使われる仕組み
1-1. 共有メモリの基本概念
共有メモリとは、複数のプロセスが同じメモリ領域を参照できるようにするプロセス間通信の方式です。
通常、プロセスごとのメモリ空間は独立しています。たとえば、アプリAの変数に入っている値を、アプリBが直接読むことはできません。これはOSがプロセス間のメモリを分離し、安全性を保っているためです。
しかし、共有メモリを使うと、OSが管理する特定のメモリ領域を複数プロセスにマッピングできます。これにより、プロセスAが共有領域に書き込んだデータを、プロセスBが同じ共有領域から読み取れます。
共有メモリは、データのコピー回数を抑えやすく、高速なプロセス間通信に向いています。ただし、複数プロセスが同時に読み書きするため、排他制御やデータ形式の設計が重要です。
1-2. C#で共有メモリを扱う代表的な方法
C#で共有メモリを扱う代表的な方法は、MemoryMappedFileクラスを使う方法です。
主に次のような実装方法があります。
C#using System.IO.MemoryMappedFiles;
MemoryMappedFileを使うと、以下のような共有メモリを作成できます。
C#using var mmf = MemoryMappedFile.CreateOrOpen("Local\\SampleMemory", 1024);
このコードでは、Local\\SampleMemoryという名前の共有メモリを、1024バイトの容量で作成またはオープンしています。
C#で共有メモリを使う場合は、単にメモリ領域を作るだけでなく、次の要素も一緒に設計します。
共有メモリ名
共有メモリサイズ
データ形式
読み書き位置
排他制御
通知方法
例外処理
リソース解放
1-3. MemoryMappedFileとは
MemoryMappedFileは、メモリマップトファイルを扱うための.NETのクラスです。ファイルの内容をアプリケーションの論理アドレス空間にマップし、巨大なファイルや共有メモリ領域へ効率的にアクセスするために使われます。
C#では、MemoryMappedFileを使うことで、ファイルベースのメモリマップトファイルだけでなく、ディスク上の実ファイルに関連付かない非永続化メモリマップトファイルも扱えます。
プロセス間通信でよく使われるのは、名前付きの非永続化メモリマップトファイルです。これは、特定の名前を持つメモリ領域を作成し、別プロセスが同じ名前で開くことでデータ共有を行います。
1-4. 共有メモリとファイルマッピングの関係
共有メモリとファイルマッピングは密接に関係しています。
メモリマップトファイルには、大きく分けて次の2種類があります。
1つ目は、ディスク上のファイルに関連付く永続化メモリマップトファイルです。これは、大容量ファイルの一部をメモリのように扱いたい場合に使います。最後のプロセスが処理を終えると、変更内容は元のファイルに保存されます。
2つ目は、ディスク上のファイルに関連付かない非永続化メモリマップトファイルです。これは、主にプロセス間通信の共有メモリとして使われます。
つまり、C#共有メモリで使うMemoryMappedFileは、ファイルをメモリに見せる仕組みを応用して、複数プロセスが同じ領域へアクセスできるようにしていると考えると理解しやすいです。
1-5. ソケット・名前付きパイプ・ファイル連携との違い
プロセス間通信には、共有メモリ以外にもソケット、名前付きパイプ、一時ファイル、WCF、gRPCなどがあります。
共有メモリは、同一マシン内で高速にデータを共有したい場合に向いています。一方、TCPソケットはネットワーク越しの通信にも対応できます。名前付きパイプは、同一マシンまたはネットワーク上のプロセス間でメッセージ指向の通信を行うときに便利です。一時ファイル連携は実装が簡単ですが、ディスクI/Oが発生するため高速性には劣ります。
共有メモリは非常に高速ですが、データの区切り、同期、排他制御を自分で設計する必要があります。そのため、単純な要求応答の通信なら名前付きパイプやgRPCのほうが扱いやすいケースもあります。
2. C#で共有メモリを使うメリットと向いているケース
2-1. プロセス間で高速にデータ共有できる
共有メモリの大きなメリットは、プロセス間で高速にデータを共有できることです。
通常の通信方式では、送信側から受信側へデータを渡すときに、シリアライズ、コピー、カーネルバッファ、受信側での復元などが発生します。一方、共有メモリでは、複数プロセスが同じメモリ領域を参照するため、データのコピーを減らしやすくなります。
たとえば、画像処理アプリと解析アプリを別プロセスで動かし、大きな画像データを頻繁に渡すような場合、共有メモリを使うことで転送コストを抑えられます。
2-2. 大容量データの受け渡しに向いている
共有メモリは、大容量データの受け渡しに向いています。
たとえば、次のようなデータです。
画像データ
音声データ
センサーデータ
ログバッファ
バイナリデータ
計測結果
大きな配列
名前付きパイプやソケットでも大容量データは送れますが、送信と受信のたびにデータコピーやバッファ管理が必要になります。共有メモリでは、あらかじめ確保した領域に直接データを書き込み、別プロセスが同じ領域から読み取るため、大容量データの受け渡しに適しています。
2-3. リアルタイム性が必要な処理で使いやすい
共有メモリは、リアルタイム性が求められる処理でも使いやすいです。
たとえば、次のような場面です。
監視アプリが常に最新状態を表示する
計測アプリが高頻度でデータを更新する
画像処理結果をビューアに即時反映する
別プロセスの状態をダッシュボードに表示する
ゲームやシミュレーションの状態を外部ツールで監視する
ただし、リアルタイム性を高めるには、共有メモリそのものだけでなく、通知方法も重要です。データ更新を受信側へ知らせるには、EventWaitHandleなどを組み合わせると効率的です。
2-4. 別アプリケーション間の連携に活用できる
C#共有メモリは、別アプリケーション間の連携にも活用できます。
たとえば、メインアプリとサブアプリを分けて、メインアプリが共有メモリに状態を書き込み、サブアプリがそれを読み取って画面表示するような構成が考えられます。
また、C#アプリ同士だけでなく、C++など別言語のアプリと連携することも可能です。ただし、その場合はデータ構造、文字コード、バイトオーダー、構造体のパディングなどを厳密に揃える必要があります。
2-5. 共有メモリを使わないほうがよいケース
共有メモリは便利ですが、すべてのプロセス間通信に向いているわけではありません。
次のような場合は、別の通信方式を検討したほうがよいです。
ネットワーク越しに通信したい
要求応答型のAPI通信を作りたい
データ形式を柔軟に変更したい
排他制御を自前で実装したくない
セキュリティを厳密に管理したい
小さなメッセージをたまに送るだけ
将来的に別マシンとの通信へ拡張したい
共有メモリは高速ですが、設計を誤るとデータ破損、競合、デッドロック、権限エラーが起きやすくなります。速度よりも保守性や拡張性を重視する場合は、名前付きパイプ、TCP、gRPCなども候補に入れるべきです。
3. MemoryMappedFileを使うための前提知識
3-1. System.IO.MemoryMappedFiles名前空間
C#でMemoryMappedFileを使うには、次の名前空間を使用します。
C#using System.IO.MemoryMappedFiles;
この名前空間には、主に以下のクラスがあります。
C#MemoryMappedFile
MemoryMappedViewAccessor
MemoryMappedViewStream
共有メモリを作るのがMemoryMappedFile、ランダムアクセスで読み書きするのがMemoryMappedViewAccessor、ストリームとして読み書きするのがMemoryMappedViewStreamです。
3-2. CreateNew・CreateOrOpen・OpenExistingの違い
MemoryMappedFileには、共有メモリを作成または開くためのメソッドが複数あります。
CreateNewは、新しいメモリマップトファイルを作成します。同じ名前のものがすでに存在する場合は例外になります。
C#using var mmf = MemoryMappedFile.CreateNew("Local\\SampleMemory", 1024);
CreateOrOpenは、指定した名前のメモリマップトファイルが存在しなければ作成し、存在すれば開きます。Microsoftの公式ドキュメントでも、同じ名前のファイルが存在する場合は既存のメモリマップトファイルを開くと説明されています。
C#using var mmf = MemoryMappedFile.CreateOrOpen("Local\\SampleMemory", 1024);
OpenExistingは、すでに存在するメモリマップトファイルを開きます。存在しない場合は例外になります。
C#using var mmf = MemoryMappedFile.OpenExisting("Local\\SampleMemory");
送信側がCreateOrOpenまたはCreateNewで共有メモリを作成し、受信側がOpenExistingで開く構成がよく使われます。
3-3. MemoryMappedViewAccessorの役割
MemoryMappedViewAccessorは、共有メモリの指定位置へランダムアクセスするためのクラスです。
整数、浮動小数点数、構造体、byte配列などを、任意のオフセット位置に読み書きできます。
C#using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, 123);
int value = accessor.ReadInt32(0);
固定長のデータ、ヘッダー領域、構造体、バイナリデータを扱う場合は、MemoryMappedViewAccessorが使いやすいです。
3-4. MemoryMappedViewStreamの役割
MemoryMappedViewStreamは、共有メモリをストリームとして扱うためのクラスです。
C#using var stream = mmf.CreateViewStream();
stream.Write(buffer, 0, buffer.Length);
byte配列を順番に読み書きする場合や、BinaryReader、BinaryWriterと組み合わせたい場合に便利です。
ただし、共有メモリ上で複数データを扱う場合は、現在位置やデータ長の管理が必要です。単純なメッセージ送受信であればViewStream、オフセットを明確に管理したい場合はViewAccessorを使うとよいでしょう。
3-5. 名前付き共有メモリと匿名共有メモリの違い
名前付き共有メモリは、共有メモリに名前を付けて、別プロセスから同じ名前で開けるようにする方式です。
C#MemoryMappedFile.CreateOrOpen("Local\\MySharedMemory", 4096);
一方、匿名共有メモリは名前を持たず、通常はハンドルを子プロセスへ渡すような用途で使います。
プロセス間通信で扱いやすいのは名前付き共有メモリです。ただし、.NETにおける名前付きメモリマップトファイルの扱いはOS依存の点があります。公式ドキュメントでは、OpenExistingなど一部の名前付きメモリマップトファイルAPIにWindows向けのサポート属性が付いているため、クロスプラットフォーム対応が必要な場合は、対象OSでの動作確認が必須です。
4. C#共有メモリの基本実装例
4-1. 共有メモリを作成するサンプルコード
まずは、C#で共有メモリを作成する最小コードです。
C#using System;
using System.IO.MemoryMappedFiles;
class Program
{
static void Main()
{
const string mapName = "Local\\CSharpSharedMemorySample";
const long capacity = 1024;
using var mmf = MemoryMappedFile.CreateOrOpen(mapName, capacity);
Console.WriteLine("共有メモリを作成しました。Enterで終了します。");
Console.ReadLine();
}
}
mapNameは共有メモリの名前です。別プロセスからアクセスするときも、同じ名前を指定します。
capacityは共有メモリのサイズです。あとから簡単に拡張できるものではないため、扱うデータの最大サイズを考えて設計します。
4-2. 共有メモリへ文字列を書き込む方法
文字列を書き込む場合は、文字列をbyte配列に変換してから共有メモリへ書き込みます。
C#using System;
using System.IO.MemoryMappedFiles;
using System.Text;
class Writer
{
static void Main()
{
const string mapName = "Local\\CSharpSharedMemorySample";
const long capacity = 4096;
using var mmf = MemoryMappedFile.CreateOrOpen(mapName, capacity);
using var accessor = mmf.CreateViewAccessor();
string message = "こんにちは、共有メモリ";
byte[] data = Encoding.UTF8.GetBytes(message);
accessor.Write(0, data.Length);
accessor.WriteArray(4, data, 0, data.Length);
Console.WriteLine("共有メモリへ書き込みました。");
Console.ReadLine();
}
}
この例では、先頭4バイトに文字列データの長さを書き込み、その後ろにUTF-8の文字列データを書き込んでいます。
共有メモリでは、文字列の終端や長さを自動で管理してくれません。そのため、データ長を明示的に持つ設計にするのが安全です。
4-3. 別プロセスから共有メモリを読み取る方法
次に、別プロセスから共有メモリを読み取るコードです。
C#using System;
using System.IO.MemoryMappedFiles;
using System.Text;
class Reader
{
static void Main()
{
const string mapName = "Local\\CSharpSharedMemorySample";
using var mmf = MemoryMappedFile.OpenExisting(mapName);
using var accessor = mmf.CreateViewAccessor();
int length = accessor.ReadInt32(0);
byte[] data = new byte[length];
accessor.ReadArray(4, data, 0, length);
string message = Encoding.UTF8.GetString(data);
Console.WriteLine($"読み取った文字列: {message}");
}
}
このコードでは、先頭4バイトからデータ長を読み取り、その長さ分のbyte配列を読み込んで文字列へ変換しています。
送信側が共有メモリを作成していない状態で受信側を実行すると、OpenExistingで例外が発生します。そのため、実運用では例外処理や起動順序の制御が必要です。
4-4. byte配列を共有する実装例
画像やバイナリデータを扱う場合は、byte配列をそのまま共有メモリへ書き込むことが多いです。
C#byte[] buffer = new byte[] { 1, 2, 3, 4, 5 };
using var mmf = MemoryMappedFile.CreateOrOpen("Local\\ByteArrayMemory", 1024);
using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, buffer.Length);
accessor.WriteArray(4, buffer, 0, buffer.Length);
読み取り側は次のように実装します。
C#using var mmf = MemoryMappedFile.OpenExisting("Local\\ByteArrayMemory");
using var accessor = mmf.CreateViewAccessor();
int length = accessor.ReadInt32(0);
byte[] buffer = new byte[length];
accessor.ReadArray(4, buffer, 0, length);
byte配列を扱う場合も、先頭に長さを持たせる設計にすると読み取り側が安全に処理できます。
4-5. 構造体データを共有する実装例
固定長のデータであれば、構造体を共有メモリに書き込むこともできます。
C#using System;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SensorData
{
public int Id;
public double Value;
public long Timestamp;
}
class Program
{
static void Main()
{
const string mapName = "Local\\StructMemory";
var data = new SensorData
{
Id = 1,
Value = 123.45,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
using var mmf = MemoryMappedFile.CreateOrOpen(mapName, 1024);
using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, ref data);
SensorData readData;
accessor.Read(0, out readData);
Console.WriteLine($"{readData.Id}, {readData.Value}, {readData.Timestamp}");
}
}
構造体を共有する場合は、構造体のレイアウトを明確にするためにStructLayoutを指定するのが基本です。
ただし、参照型のフィールドを含むクラスや、可変長文字列を含む複雑なオブジェクトをそのまま共有するのは避けるべきです。共有メモリに置けるのは、基本的にはバイト列として解釈できるデータです。
4-6. usingによるリソース解放の書き方
MemoryMappedFile、MemoryMappedViewAccessor、MemoryMappedViewStreamは、使い終わったら解放する必要があります。
C#ではusingまたはusing varを使うと安全です。
C#using var mmf = MemoryMappedFile.CreateOrOpen("Local\\Sample", 1024);
using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, 100);
古い書き方では次のように書けます。
C#using (var mmf = MemoryMappedFile.CreateOrOpen("Local\\Sample", 1024))
using (var accessor = mmf.CreateViewAccessor())
{
accessor.Write(0, 100);
}
共有メモリは、最後に参照しているプロセスが閉じるまで存在します。不要になったViewやMemoryMappedFileを解放しないと、リソースリークやファイルロックの原因になります。
5. MemoryMappedFileでプロセス間通信を行う実装手順
5-1. 送信側プロセスの実装
送信側プロセスでは、共有メモリを作成し、データを書き込みます。
C#using System;
using System.IO.MemoryMappedFiles;
using System.Text;
using System.Threading;
class Sender
{
static void Main()
{
const string mapName = "Local\\IpcSampleMemory";
const string mutexName = "Local\\IpcSampleMutex";
const long capacity = 4096;
using var mmf = MemoryMappedFile.CreateOrOpen(mapName, capacity);
using var mutex = new Mutex(false, mutexName);
string message = $"送信時刻: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
byte[] data = Encoding.UTF8.GetBytes(message);
mutex.WaitOne();
try
{
using var accessor = mmf.CreateViewAccessor();
accessor.Write(0, data.Length);
accessor.WriteArray(4, data, 0, data.Length);
}
finally
{
mutex.ReleaseMutex();
}
Console.WriteLine("送信しました。");
Console.ReadLine();
}
}
この例では、Mutexを使って書き込み中に別プロセスが読み取らないようにしています。
5-2. 受信側プロセスの実装
受信側プロセスでは、既存の共有メモリを開いてデータを読み取ります。
C#using System;
using System.IO.MemoryMappedFiles;
using System.Text;
using System.Threading;
class Receiver
{
static void Main()
{
const string mapName = "Local\\IpcSampleMemory";
const string mutexName = "Local\\IpcSampleMutex";
using var mmf = MemoryMappedFile.OpenExisting(mapName);
using var mutex = new Mutex(false, mutexName);
mutex.WaitOne();
try
{
using var accessor = mmf.CreateViewAccessor();
int length = accessor.ReadInt32(0);
if (length <= 0 || length > 4092)
{
throw new InvalidOperationException("データサイズが不正です。");
}
byte[] data = new byte[length];
accessor.ReadArray(4, data, 0, length);
string message = Encoding.UTF8.GetString(data);
Console.WriteLine($"受信: {message}");
}
finally
{
mutex.ReleaseMutex();
}
}
}
読み取り側では、必ずデータ長を検証します。共有メモリの内容が壊れていたり、書き込み途中の値を読んだりすると、不正な長さになる可能性があるためです。
5-3. 共有メモリ名を決めるときの注意点
共有メモリ名は、送信側と受信側で完全に一致している必要があります。
Windowsでは、名前にLocal\\やGlobal\\を付けることがあります。
C#Local\\MySharedMemory
Global\\MySharedMemory
Local\\は現在のログオンセッション内で使う名前空間です。Global\\は複数セッションをまたぐ用途で使われますが、環境によっては権限が必要になります。
共有メモリ名を決めるときは、他アプリケーションと衝突しないように、会社名、製品名、機能名などを含めると安全です。
C#Local\\MyCompany.MyProduct.TelemetryBuffer
5-4. データサイズを固定する設計方法
共有メモリは、作成時に容量を指定します。
C#MemoryMappedFile.CreateOrOpen("Local\\Sample", 4096);
この容量を超えて書き込むことはできません。そのため、共有するデータの最大サイズを事前に決める必要があります。
よくある設計は、先頭にヘッダー領域を置き、その後ろにデータ領域を置く方法です。
0-3 : データ長
4-7 : ステータス
8-15 : タイムスタンプ
16以降 : データ本体
このように固定レイアウトにしておくと、読み取り側も安全にデータを解釈できます。
5-5. 読み書き位置を管理する方法
共有メモリでは、どの位置に何を書き込むかを自分で管理します。
たとえば、以下のようにオフセットを定数化しておくと、コードの可読性が上がります。
C#const int OffsetLength = 0;
const int OffsetStatus = 4;
const int OffsetTimestamp = 8;
const int OffsetData = 16;
書き込み側は次のように使います。
C#accessor.Write(OffsetLength, data.Length);
accessor.Write(OffsetTimestamp, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.WriteArray(OffsetData, data, 0, data.Length);
読み取り側も同じオフセット定義を使います。
共有メモリのレイアウトは、送信側と受信側で必ず一致させる必要があります。可能であれば、共通ライブラリに定数や構造体をまとめると安全です。
5-6. 複数データを連続して扱う方法
複数のデータを共有メモリに連続して格納する場合は、データごとに長さを持たせる方法がよく使われます。
[データ1の長さ][データ1本体][データ2の長さ][データ2本体][データ3の長さ][データ3本体]
ただし、この方式では読み書き位置の管理が複雑になります。
リアルタイムに複数データを流す場合は、リングバッファ方式も有効です。リングバッファでは、書き込み位置と読み取り位置をヘッダーに持たせ、バッファ末尾まで到達したら先頭に戻って書き込みます。
6. 共有メモリで必須となる排他制御と同期処理
6-1. 共有メモリで排他制御が必要な理由
共有メモリは、複数プロセスが同じメモリ領域へアクセスします。そのため、書き込み中に別プロセスが読み取ると、データが途中の状態で読まれる可能性があります。
たとえば、送信側が先頭4バイトにデータ長を書き込み、その後に本文を書き込む設計の場合、受信側がデータ長だけ更新されたタイミングで本文を読んでしまうと、不完全なデータを取得してしまいます。
このような競合を防ぐために、Mutex、EventWaitHandle、Semaphoreなどの同期機構を組み合わせます。.NETには共有リソースへのアクセス同期やスレッド・プロセス間の調整に使える同期プリミティブが用意されています。
6-2. Mutexを使った同期処理の実装例
Mutexは、同時に1つの実行主体だけが共有リソースへアクセスできるようにするための同期機構です。
C#using var mutex = new Mutex(false, "Local\\SampleMutex");
mutex.WaitOne();
try
{
// 共有メモリへの読み書き
}
finally
{
mutex.ReleaseMutex();
}
共有メモリでは、読み書き処理全体をMutexで保護します。
C#mutex.WaitOne();
try
{
accessor.Write(0, data.Length);
accessor.WriteArray(4, data, 0, data.Length);
}
finally
{
mutex.ReleaseMutex();
}
ReleaseMutexは必ずfinallyで呼び出します。例外が発生したときにMutexが解放されないと、他プロセスが永遠に待ち続ける可能性があります。
6-3. EventWaitHandleで読み書きタイミングを通知する方法
Mutexは排他制御に使いますが、データが更新されたことを通知するにはEventWaitHandleが便利です。
送信側は、共有メモリへ書き込んだ後にイベントを通知します。
C#using var dataReady = new EventWaitHandle(
false,
EventResetMode.AutoReset,
"Local\\SampleDataReadyEvent");
dataReady.Set();
受信側は、イベントが通知されるまで待機します。
C#using var dataReady = new EventWaitHandle(
false,
EventResetMode.AutoReset,
"Local\\SampleDataReadyEvent");
dataReady.WaitOne();
// 通知後に共有メモリを読み取る
AutoResetは、待機中の1つのスレッドまたはプロセスを起こしたあと、自動的に非シグナル状態へ戻ります。1対1通信では扱いやすい設定です。
複数の受信側へ同時に通知したい場合は、ManualResetを検討します。ただし、リセットタイミングを誤ると通知漏れや重複処理の原因になります。
6-4. Semaphoreを使うケース
Semaphoreは、同時にアクセスできる数を制限したい場合に使います。Microsoftのドキュメントでも、SemaphoreおよびSemaphoreSlimは、共有リソースやリソースプールへ同時にアクセスできるスレッド数を制限するためのクラスとして説明されています。
共有メモリでSemaphoreを使うケースとしては、複数の読み取りプロセスがあり、同時読み取り数を制限したい場合や、複数スロットのバッファを管理する場合が考えられます。
ただし、単純な1対1通信ではMutexとEventWaitHandleで十分なことが多いです。
6-5. デッドロックを防ぐ実装上の注意点
共有メモリの同期処理では、デッドロックに注意が必要です。
デッドロックを防ぐポイントは次の通りです。
Mutexを取得したら必ずfinallyで解放する
複数のMutexを使う場合は取得順序を統一する
Mutexを保持したまま長時間処理しない
共有メモリの読み書き中にUI操作やネットワーク待機をしない
WaitOneにタイムアウトを設定する
例外発生時も状態を復旧できるようにする
たとえば、次のようにタイムアウト付きで待機すると、異常時に永久待機を避けられます。
C#if (!mutex.WaitOne(TimeSpan.FromSeconds(5)))
{
throw new TimeoutException("共有メモリのロック取得に失敗しました。");
}
try
{
// 共有メモリ処理
}
finally
{
mutex.ReleaseMutex();
}
6-6. 読み込み中・書き込み中の競合を防ぐ設計
読み込み中・書き込み中の競合を防ぐには、共有メモリ上にステータスを持たせる方法もあります。
0-3 : データ長
4-7 : ステータス
8-15 : バージョン番号
16- : データ本体
ステータスには、たとえば次の値を使います。
0: 空
1: 書き込み中
2: 書き込み完了
3: 読み取り中
ただし、ステータス自体の更新も競合するため、基本的にはMutexと組み合わせます。
より堅牢にするなら、書き込み前後でバージョン番号を更新し、読み取り側が前後のバージョンを比較する方法もあります。
7. C#共有メモリ実装でよくあるエラーと対処法
7-1. OpenExistingで共有メモリが見つからない
OpenExistingで共有メモリが見つからない場合、主な原因は次の通りです。
送信側プロセスが起動していない
共有メモリ名が一致していない
Local\\とGlobal\\の指定が違う送信側がすでに終了して共有メモリが破棄された
OSや実行環境で名前付きメモリマップトファイルが使えない
対処法としては、受信側で例外を捕捉し、一定時間リトライする実装が有効です。
C#MemoryMappedFile? mmf = null;
for (int i = 0; i < 10; i++)
{
try
{
mmf = MemoryMappedFile.OpenExisting("Local\\SampleMemory");
break;
}
catch (FileNotFoundException)
{
Thread.Sleep(500);
}
}
if (mmf == null)
{
throw new InvalidOperationException("共有メモリを開けませんでした。");
}
7-2. UnauthorizedAccessExceptionが発生する
UnauthorizedAccessExceptionは、アクセス権限が不足している場合に発生します。
たとえば、別ユーザー、別セッション、サービスプロセス、管理者権限プロセスとの間で共有メモリを使う場合に起きやすいです。
対処法は次の通りです。
同じユーザー権限で実行する
Local\\とGlobal\\の使い分けを確認する必要に応じてアクセス制御を設定する
管理者権限の有無を揃える
サービスとデスクトップアプリ間の通信では権限設計を見直す
共有メモリを安易にGlobal\\で公開すると、想定外のプロセスからアクセスされるリスクがあります。アクセス制御は慎重に設計しましょう。
7-3. IOExceptionが発生する
IOExceptionは、共有メモリの作成やアクセスで失敗したときに発生することがあります。
よくある原因は次の通りです。
同じ名前の共有メモリがすでに存在する
指定した容量やアクセス権が既存の共有メモリと合わない
ファイルベースのメモリマップトファイルで対象ファイルにアクセスできない
ビューの作成範囲が不正
リソースが不足している
CreateNewは同名の共有メモリがあると失敗するため、既存のものを再利用したい場合はCreateOrOpenを使います。
7-4. 書き込みサイズ超過によるエラー
共有メモリの容量を超えて書き込むとエラーになります。
たとえば、容量4096バイトの共有メモリに対して、ヘッダー4バイトと本文4096バイトを書き込もうとすると、合計4100バイトになり容量を超えます。
書き込み前には必ずサイズを検証します。
C#const int capacity = 4096;
const int headerSize = 4;
if (data.Length > capacity - headerSize)
{
throw new InvalidOperationException("データサイズが共有メモリ容量を超えています。");
}
共有メモリでは、サイズ超過を防ぐ設計が非常に重要です。
7-5. 文字化け・データ破損が起きる原因
文字化けやデータ破損の主な原因は次の通りです。
書き込み側と読み取り側で文字コードが違う
データ長の管理が間違っている
書き込み途中に読み取っている
前回のデータが残っている
構造体のレイアウトが一致していない
32bitと64bitで構造体サイズが変わっている
排他制御が不足している
文字列を扱う場合は、UTF-8など文字コードを明示的に決めましょう。
C#byte[] data = Encoding.UTF8.GetBytes(message);
string text = Encoding.UTF8.GetString(data);
また、可変長データでは、必ず長さを保存します。null終端に頼る設計は、バイナリデータやマルチバイト文字で問題が起きやすいため注意が必要です。
7-6. 32bit・64bit環境で注意すべき点
共有メモリで構造体を扱う場合、32bit環境と64bit環境の違いに注意が必要です。
特に注意するべきなのは、ポインタサイズ、構造体のアライメント、IntPtr、long、パディングです。
C#アプリ同士であっても、片方がx86、もう片方がx64で動いていると、構造体サイズやレイアウトの想定がずれる可能性があります。
対策としては、次のような方法があります。
両プロセスのプラットフォームターゲットを揃える
構造体に
StructLayoutを指定する参照型やポインタを共有データに含めない
共有データはbyte配列ベースにする
明示的なシリアライズ形式を使う
プロセス間で安全に扱いたい場合は、構造体を直接共有するよりも、明確なバイナリフォーマットを定義するほうが堅牢です。
8. MemoryMappedFileを安全に使うための注意点
8-1. 共有メモリのサイズ設計
共有メモリのサイズは、最初に慎重に設計する必要があります。
サイズが小さすぎると、すぐに書き込み上限に達します。大きすぎると、管理が雑になり、無駄な領域やセキュリティリスクが増えます。
おすすめは、ヘッダー領域とデータ領域を分け、最大データサイズを定数で管理することです。
C#const int HeaderSize = 16;
const int MaxDataSize = 4096;
const int Capacity = HeaderSize + MaxDataSize;
このようにすると、書き込み前のサイズチェックも実装しやすくなります。
8-2. データ形式を明確に決める
共有メモリでは、データ形式を必ず明確に決めます。
たとえば、次のような仕様を決めておきます。
文字コード: UTF-8
先頭4バイト: データ長
次の4バイト: ステータス
次の8バイト: タイムスタンプ
以降: データ本体
データ形式が曖昧だと、送信側と受信側の実装が少し変わっただけでデータ破損につながります。
特に複数アプリや複数言語で共有する場合は、仕様書や共通ライブラリを用意するのがおすすめです。
8-3. 例外処理を必ず実装する
共有メモリ処理では、例外処理が欠かせません。
想定される例外には、次のようなものがあります。
FileNotFoundExceptionUnauthorizedAccessExceptionIOExceptionTimeoutExceptionArgumentExceptionInvalidOperationException
特に受信側は、共有メモリがまだ作成されていない状態で起動されることがあります。そのため、OpenExistingを使う場合はリトライ処理を検討しましょう。
8-4. DisposeとCloseでリソースリークを防ぐ
MemoryMappedFileやViewAccessorは、使用後に必ず解放します。
C#using var mmf = MemoryMappedFile.CreateOrOpen("Local\\Sample", 1024);
using var accessor = mmf.CreateViewAccessor();
usingを使えば、スコープを抜けたときに自動的にDisposeされます。
共有メモリを参照しているプロセスが残っている間は、メモリマップトファイルも残ります。不要な参照を残さないよう、ViewやMemoryMappedFileを適切に解放しましょう。
8-5. セキュリティ権限とアクセス制御
共有メモリは、同じマシン上の別プロセスからアクセスされる可能性があります。
そのため、共有メモリ名を推測しやすい名前にしたり、広い権限で公開したりすると、意図しないプロセスに読み書きされるリスクがあります。
特に、Global\\名前空間を使う場合や、サービスとユーザーアプリ間で共有する場合は、アクセス権限の設計が重要です。
共有メモリを作成するプロセス、読み取るプロセス、書き込むプロセスを明確にし、必要最小限の権限にしてください。
8-6. 機密情報を共有メモリに置くリスク
共有メモリに機密情報を置く場合は注意が必要です。
たとえば、次のような情報は共有メモリに置くべきではない場合があります。
パスワード
アクセストークン
個人情報
秘密鍵
認証情報
顧客データ
共有メモリは高速ですが、セキュリティ境界としては慎重に扱う必要があります。
どうしても機密情報を扱う場合は、アクセス制御、暗号化、利用後のゼロクリア、ログ出力の禁止などを検討しましょう。
9. C#共有メモリの実用的な設計パターン
9-1. 1対1通信の設計
1対1通信は、1つの送信側と1つの受信側で共有メモリを使うパターンです。
最もシンプルな設計は、次の構成です。
共有メモリ: 1個
Mutex: 1個
EventWaitHandle: 1個
送信側は、Mutexを取得して共有メモリに書き込み、書き込み完了後にEventWaitHandleで通知します。
受信側は、EventWaitHandleで待機し、通知されたらMutexを取得して共有メモリを読み取ります。
この方式は、単一メッセージの受け渡しや、最新状態の共有に向いています。
9-2. 1対多通信の設計
1対多通信では、1つの送信側に対して複数の受信側が共有メモリを読み取ります。
この場合、書き込み側は1つでも、読み取り側が複数あるため、通知と読み取りタイミングの設計が難しくなります。
単に最新値を表示するだけなら、共有メモリには常に最新データを書き込み、受信側は定期的に読み取る方式でも構いません。
一方、すべての受信側がすべてのメッセージを処理する必要がある場合は、受信側ごとの読み取り位置を管理する必要があります。
9-3. 双方向通信を実装する方法
双方向通信を実装する場合は、共有メモリを2つ用意する方法がわかりやすいです。
ClientToServerMemory
ServerToClientMemory
クライアントからサーバーへ送る領域と、サーバーからクライアントへ返す領域を分けます。
同期オブジェクトも方向ごとに分けると管理しやすくなります。
ClientToServerMutex
ServerToClientMutex
ClientToServerEvent
ServerToClientEvent
1つの共有メモリで双方向通信を実装することもできますが、読み書きの競合や状態管理が複雑になるため、最初は方向ごとに分ける設計がおすすめです。
9-4. リングバッファ方式でデータを共有する方法
高頻度で複数のデータを共有する場合は、リングバッファ方式が有効です。
リングバッファでは、固定長のバッファを循環利用します。
ヘッダー:
- writePosition
- readPosition
- capacity
- count
データ領域:
- 固定長または可変長のデータ
書き込み側はwritePositionへデータを書き込み、位置を進めます。末尾に到達したら先頭に戻ります。
読み取り側はreadPositionからデータを読み取り、位置を進めます。
リングバッファは効率的ですが、空き容量、上書き、読み取り遅延、複数受信者の管理が必要です。ログやストリーミングデータには向いていますが、実装難易度は高めです。
9-5. ヘッダー領域とデータ領域を分ける設計
共有メモリでは、ヘッダー領域とデータ領域を分ける設計が実用的です。
0-3 : マジックナンバー
4-7 : バージョン
8-11 : データ長
12-15 : ステータス
16-23 : タイムスタンプ
24-31 : シーケンス番号
32以降 : データ本体
マジックナンバーを入れておくと、読み取り側が正しい共有メモリを開いているか確認できます。
バージョン番号を入れておくと、データ形式を将来変更するときに互換性を判断できます。
シーケンス番号を入れておくと、受信側が同じデータを重複処理したかどうかを判定できます。
9-6. ログ・監視・画像データ共有への応用例
C#共有メモリは、さまざまな用途に応用できます。
ログ共有では、アプリケーションが共有メモリ上のリングバッファにログを書き込み、ビューアがリアルタイムに表示します。
監視用途では、サービスプロセスがCPU使用率、処理件数、エラー数などを書き込み、管理ツールが読み取って表示します。
画像データ共有では、画像処理プロセスがフレームデータを書き込み、表示プロセスが共有メモリから読み取って画面描画します。
このように、共有メモリは「大きなデータ」または「高頻度で更新されるデータ」を同一マシン内で共有したい場合に特に効果を発揮します。
10. C#共有メモリと他のプロセス間通信手段の比較
10-1. 共有メモリと名前付きパイプの違い
名前付きパイプは、プロセス間でストリーム型またはメッセージ型の通信を行う仕組みです。
共有メモリは、同じメモリ領域を複数プロセスが参照します。一方、名前付きパイプは、送信側から受信側へデータを流す通信路です。
共有メモリは高速で大容量データに向いていますが、データ管理と同期処理を自分で実装する必要があります。
名前付きパイプは、要求応答やメッセージ送受信を実装しやすく、共有メモリよりも扱いやすいケースがあります。
10-2. 共有メモリとTCPソケットの違い
TCPソケットは、同一マシンだけでなくネットワーク越しの通信にも対応できます。
共有メモリは基本的に同一マシン内のプロセス間通信に使います。そのため、別マシンとの通信が必要な場合はTCPソケット、HTTP、gRPCなどを使うべきです。
一方、同一マシン内で大容量データを高速に共有したい場合は、共有メモリが有利です。
10-3. 共有メモリと一時ファイル連携の違い
一時ファイル連携は、送信側がファイルを書き出し、受信側がそのファイルを読む方式です。
実装は簡単ですが、ディスクI/Oが発生し、ファイル削除や排他制御も必要になります。
共有メモリは、ディスクを介さずにデータ共有できるため高速です。ただし、プロセス終了後にデータを残したい場合は、一時ファイルや通常ファイルのほうが向いています。
10-4. 共有メモリとWCF・gRPCの違い
WCFやgRPCは、サービス間通信やAPI通信に向いた仕組みです。
データ形式、呼び出しインターフェース、エラー処理、拡張性などが整理されており、保守しやすい通信を作れます。
一方、共有メモリは低レベルなデータ共有方式です。高速ですが、APIとしての設計やバージョニング、エラー応答などは自分で作る必要があります。
業務アプリのサービス間通信ではgRPC、同一マシン内の大容量データ共有では共有メモリ、というように使い分けるとよいでしょう。
10-5. 要件別に選ぶべき通信方式
通信方式は、要件に応じて選びます。
高速な大容量データ共有が必要なら共有メモリが向いています。
単純なプロセス間メッセージ通信なら名前付きパイプが扱いやすいです。
ネットワーク越しに通信するならTCP、HTTP、gRPCが候補になります。
データを永続化したいならファイルやデータベースを使います。
小規模な連携で速度要件が低いなら、一時ファイルやJSONファイル連携でも十分な場合があります。
重要なのは、共有メモリを「高速だから」という理由だけで選ばないことです。同期、排他、権限、データ形式、保守性まで含めて判断しましょう。
11. C#共有メモリのパフォーマンス最適化
11-1. 不要なコピーを減らす
共有メモリの性能を活かすには、不要なコピーを減らすことが重要です。
たとえば、データを何度もbyte配列へコピーしたり、文字列変換を繰り返したりすると、共有メモリのメリットが薄れます。
可能であれば、バイナリ形式のまま読み書きし、必要なタイミングでだけ変換します。
11-2. アクセス範囲を小さくする
MemoryMappedFileでは、必要な範囲だけViewを作成できます。
巨大な共有メモリ全体にアクセスするのではなく、必要な範囲だけを扱うと、設計が明確になります。
C#using var accessor = mmf.CreateViewAccessor(offset, size);
大容量データを扱う場合は、全体を一度に処理するのではなく、チャンク単位で処理する方法も検討しましょう。
11-3. 同期処理の待ち時間を短くする
共有メモリの性能は、同期処理の設計にも大きく影響されます。
Mutexを取得している時間が長いと、他プロセスが待たされます。
Mutex内では、共有メモリへの読み書きだけを行い、時間のかかる処理はロック外で実行するのが基本です。
悪い例は、Mutexを取得したままログ出力、ファイル保存、ネットワーク通信、UI更新を行うことです。
良い設計では、共有メモリから必要なデータだけを素早くコピーし、Mutexを解放してから後続処理を行います。
11-4. 大容量データを扱うときのポイント
大容量データを扱うときは、次の点を意識します。
共有メモリサイズを十分に確保する
データ長を必ず検証する
チャンク単位で処理する
不要な文字列変換を避ける
byte配列の再利用を検討する
更新頻度と読み取り頻度を調整する
読み取り側が遅れたときの挙動を決める
特に画像や音声のような連続データでは、読み取り側が処理に追いつかない場合があります。この場合、古いデータを破棄して最新データだけを読むのか、すべてのデータを処理するのかを事前に決めておく必要があります。
11-5. ベンチマークで確認すべき項目
共有メモリを導入するときは、必ずベンチマークを行います。
確認すべき項目は次の通りです。
書き込み時間
読み取り時間
同期待ち時間
データサイズごとの処理時間
1秒あたりの更新回数
CPU使用率
メモリ使用量
受信側が遅れたときの挙動
例外発生時の復旧時間
共有メモリは高速ですが、設計次第ではMutex待ちやコピー処理がボトルネックになります。速度だけでなく、安定性も含めて測定しましょう。
12. C#共有メモリに関するよくある質問
12-1. MemoryMappedFileはプロセス終了後も残るのか
非永続化の名前付きMemoryMappedFileは、参照しているプロセスがすべて閉じると破棄されます。
つまり、作成したプロセスが終了しても、別プロセスがまだ開いていれば残ります。一方、すべてのプロセスが閉じれば消えます。
ディスク上のファイルに関連付く永続化メモリマップトファイルの場合は、最終的に変更内容がファイルへ反映されます。
12-2. 別ユーザーのプロセスから共有メモリにアクセスできるのか
別ユーザーのプロセスからアクセスできるかどうかは、名前空間、実行セッション、アクセス権限に依存します。
同一ユーザー、同一セッション内であればLocal\\の名前付き共有メモリで扱いやすいです。
別ユーザーやサービスプロセスと共有する場合は、Global\\名前空間やアクセス制御を検討する必要があります。
ただし、権限を広げるほどセキュリティリスクも増えるため、必要最小限のアクセスにすることが重要です。
12-3. .NET Frameworkと.NETで実装方法は違うのか
基本的な考え方は同じです。
どちらもSystem.IO.MemoryMappedFiles名前空間のMemoryMappedFileを使って実装できます。
ただし、対象OS、APIのサポート状況、セキュリティ関連の扱い、プロジェクト形式、例外の挙動などに違いが出る場合があります。特に.NETでクロスプラットフォーム対応する場合は、Windows以外で名前付きメモリマップトファイルが使えるかを確認する必要があります。公式ドキュメントでは、OpenExistingなどにWindowsサポートを示す属性が付いています。
12-4. 共有メモリでクラスやオブジェクトをそのまま共有できるのか
C#のクラスやオブジェクトをそのまま共有メモリに置いて、別プロセスから同じオブジェクトとして扱うことはできません。
プロセスごとにアドレス空間が異なるため、参照型のアドレスは別プロセスでは意味を持ちません。
共有メモリで扱うべきなのは、byte配列、数値、固定レイアウトの構造体など、バイト列として表現できるデータです。
クラスを共有したい場合は、JSON、MessagePack、独自バイナリ形式などにシリアライズして共有メモリへ書き込み、読み取り側で復元します。
12-5. 共有メモリはスレッド間通信にも使えるのか
同一プロセス内のスレッド間通信にも共有メモリの考え方は使えますが、通常はMemoryMappedFileを使う必要はありません。
同一プロセス内であれば、通常のオブジェクト、ConcurrentQueue、Channel、lock、Monitor、SemaphoreSlimなどを使うほうが簡単です。
MemoryMappedFileは、主にプロセスをまたいでデータを共有したい場合に使うものです。
12-6. C#で共有メモリを使うときに最初に確認すべきこと
C#で共有メモリを使う前に、まず次の点を確認しましょう。
本当に共有メモリが必要か
同一マシン内の通信か
大容量または高頻度のデータ共有が必要か
データ形式を固定できるか
排他制御を正しく実装できるか
セキュリティリスクを許容できるか
名前付きパイプやgRPCでは不十分か
対象OSで必要なAPIが使えるか
共有メモリは強力ですが、設計責任も大きい仕組みです。速度、保守性、安全性のバランスを考えて採用しましょう。
まとめ
C#で共有メモリを使う場合は、MemoryMappedFileを利用するのが基本です。MemoryMappedFileを使うことで、複数プロセスが同じメモリ領域を参照し、高速にデータを共有できます。
共有メモリは、大容量データの受け渡し、リアルタイム監視、画像データ共有、ログバッファ、別アプリケーション間連携などに向いています。一方で、データ形式、サイズ、読み書き位置、排他制御、通知、例外処理、セキュリティを自分で設計する必要があります。
基本的な実装では、送信側がCreateOrOpenで共有メモリを作成し、受信側がOpenExistingで開きます。データの読み書きにはMemoryMappedViewAccessorやMemoryMappedViewStreamを使います。
実用的なプロセス間通信では、共有メモリ単体では不十分です。Mutexで排他制御し、EventWaitHandleで更新通知を行い、必要に応じてSemaphoreやリングバッファを組み合わせます。
C#共有メモリを安全に使うには、次のポイントが重要です。
共有メモリサイズを明確に設計する
データ形式を固定する
データ長を必ず検証する
Mutexで読み書きを保護する
EventWaitHandleで通知する
usingで確実にリソースを解放する
権限とセキュリティを考慮する
機密情報を安易に置かない
対象OSでのサポート状況を確認する
共有メモリは、正しく設計すれば非常に高速で便利なプロセス間通信手段です。C#で高性能なアプリケーション間連携を実装したい場合は、MemoryMappedFileを有力な選択肢として検討してみてください。

