【上級プロフェッショナル向け】VB.NETにおけるMemoryMappedFileを活用したプロセス間通信:複数アプリ間で大容量データを数マイクロ秒で超高速共有するメカニズム
レガシーな業務システムにおいて、複数の独立したプロセス(例えば、基幹データの常時監視エンジンと、ユーザーインターフェースを持つクライアント端末)の間でデータを同期させる際、私たちは長らく泥臭い手段に依存してきた。
名前付きパイプ(Named Pipes)、TCP/IPソケット通信、あるいは最悪なケースでは「共有フォルダへのCSVファイル吐き出しとファイル監視」である。
これらはすべて、OSのカーネル空間とユーザー空間の間でのコンテキストスイッチ、データシリアライズ、そして何よりもディスクI/Oやネットワークスタックという「致命的なボトルネック」を内包している。データ量が数メガバイトを超えた瞬間、システムは硬直し、業務端末としての応答速度は致命的に悪化する。
真にスケーラブルで、極限のパフォーマンスが求められるエンタープライズ環境において、我々が選択すべき究極のIPC(Inter-Process Communication)手段はただ一つ、MemoryMappedFile(メモリマップトファイル)だ。
今回は、VB.NETを用いてOSの仮想メモリ空間を直接共有し、複数プロセス間で大容量データを数マイクロ秒(μs)オーダーで同期させるための実践的かつ極限まで最適化されたメカニズムを解説する。
—
1. なぜメモリマップトファイルなのか?(アーキテクチャの真髄)
メモリマップトファイルの本質は、「ストレージ上のファイル、あるいはページファイルの特定領域を、複数のプロセスの仮想アドレス空間に直接マッピングする」ことにある。
通常のファイル読み書きは以下のプロセスを経る:
1. アプリケーションメモリからOSのバッファへコピー
2. ストレージコントローラーへ転送(物理I/O)
一方、メモリマップトファイルを用いた場合:
1. プロセスAが共有メモリ領域に書き込む(CPUのキャッシュラインレベルでの直接操作)
2. プロセスBが同じ仮想アドレス領域を即座に参照する
ディスクの回転待ちも、ネットワークパケットのヘッダ処理も存在しない。介在するのはOSの仮想メモリマネージャー(VMM)によるページテーブルの操作のみである。これにより、メモリ帯域の限界(DDR4/DDR5の数十GB/s)に近い速度でプロセス間データ共有を実現できる。
—
2. 【実装】VB.NETによる超高速IPC基盤の構築
ここでは、VB.NET(.NET Core / .NET 6/8以降、または .NET Framework 4.8)を想定し、安全かつ極限までオーバーヘッドを削ぎ落とした実装コードを提示する。
今回は、「構造化された大容量バイナリデータ」を複数プロセス間で一瞬で共有するシナリオを想定する。
共有データ構造体の定義
構造体は `LayoutKind.Sequential` または `Explicit` を用いてメモリ上のレイアウトを完全に制御し、マーシャリングのコストを排除する。
Imports System.Runtime.InteropServices
Public Structure SharedDataPacket
Public MessageId As ULong
Public StatusMessage As String
‘ 実際の業務データ(例:5MBのセンシングデータ配列を想定した先頭ポインタ領域)
‘ ※実運用では固定長バッファやオフセット管理を行う
Public DataValue As Double
End Structure
しかし、マネージドコードであるVB.NETで文字列や可変長データを扱う場合、ガベージコレクタ(GC)の影がつきまとう。数マイクロ秒を争う世界では、マネージドヒープの割り当て(Allocation)とGCの発生は「死刑宣告」に等しい。
そのため、メモリマップトファイルへのアクセスはアンマネージド・メモリ・アクセス(Unmanaged Memory Access)を駆使して直接バイト列を叩く。
—
プロセスA:データ書き込み側(パブリッシャー)
Imports System.IO.MemoryMappedFiles
Imports System.Runtime.InteropServices
Imports System.Threading
Public Class MemoryMappedPublisher
Private Const MapName As String = “Local\EnterpriseCoreDataBus”
Private Const BufferSize As Long = 1024 1024 10 ‘ 10MBの共有空間
Private m_file As MemoryMappedFile
Private m_accessor As MemoryMappedViewAccessor
Public Sub Initialize()
‘ ページファイル(またはシステムメモリ)上に名前付きメモリマップトファイルを生成
m_file = MemoryMappedFile.CreateOrOpen(
MapName,
BufferSize,
MemoryMappedFileAccess.ReadWrite,
MemoryMappedFileShare.ReadWrite
)
‘ ビューアクセサの取得(オフセット0から全領域)
m_accessor = m_file.CreateViewAccessor(0, BufferSize, MemoryMappedFileAccess.ReadWrite)
End Sub
Public Sub PublishData(id As ULong, value As Double, message As String)
‘ ガベージコレクションを誘発させないため、直接バッファに書き込む
‘ ※極限の速度を追求する場合、文字列はByte配列に変換して直接書き込むのが定石
SyncLock Me
‘ 先頭8バイトにID
m_accessor.Write(0, id)
‘ 続く8バイトにDouble値
m_accessor.Write(8, value)
‘ 文字列の書き込み(最大256バイトの固定長領域を想定)
Dim msgBytes() As Byte = System.Text.Encoding.Unicode.GetBytes(message)
Dim writeLength As Integer = Math.Min(msgBytes.Length, 256)
m_accessor.Write(16, CUShort(writeLength)) ‘ 文字列長を書き込み
‘ 安全にアンマネージドバッファへコピー
‘ ※Marshal.Copyの代わりにAccessorのWriteArrayを使用
If writeLength > 0 Then
m_accessor.WriteArray(18, msgBytes, 0, writeLength)
End If
End SyncLock
End Sub
Public Sub Dispose()
If m_accessor IsNot Nothing Then m_accessor.Dispose()
If m_file IsNot Nothing Then m_file.Dispose()
End Sub
End Class
—
プロセスB:データ読み取り側(サブスクライバー)
読み取り側は、書き込み側が生成したメモリマップトファイルにアタッチし、ポーリングあるいはイベント通知(Semaphore等との組み合わせ)によってデータを即座に取得する。
Imports System.IO.MemoryMappedFiles
Imports System.Threading
Public Class MemoryMappedSubscriber
Private Const MapName As String = “Local\EnterpriseCoreDataBus”
Private Const BufferSize As Long = 1024 1024 10
Private m_file As MemoryMappedFile
Private m_accessor As MemoryMappedViewAccessor
Public Sub Initialize()
‘ 既存のメモリマップトファイルにアタッチ(存在しない場合は例外)
m_file = MemoryMappedFile.OpenExisting(MapName, MemoryMappedFileRights.Read)
m_accessor = m_file.CreateViewAccessor(0, BufferSize, MemoryMappedFileAccess.Read)
End Sub
Public Sub ReadData(ByRef id As ULong, ByRef value As Double, ByRef message As String)
SyncLock Me
‘ ゼロコピーに近い形で直接メモリから読み出す
m_accessor.Read(0, id)
m_accessor.Read(8, value)
Dim strLen As UShort
m_accessor.Read(16, strLen)
If strLen > 0 Then
Dim msgBytes(strLen – 1) As Byte
m_accessor.ReadArray(18, msgBytes, 0, strLen)
message = System.Text.Encoding.Unicode.GetString(msgBytes)
Else
message = String.Empty
End If
End SyncLock
End Sub
Public Sub Dispose()
If m_accessor IsNot Nothing Then m_accessor.Dispose()
If m_file IsNot Nothing Then m_file.Dispose()
End Sub
End Class
—
3. シニアエンジニアが押さえるべき「極限の最適化」と罠
この実装を本番環境(特に高負荷な24時間稼働のプラント監視や金融取引システム)に投入する場合、以下の「プロの知見」を知らなければ必ずシステム障害を引き起こす。
① 競合制御(Concurrency Control)の罠
複数プロセスが同時に同一メモリ領域を読み書きする場合、「ダーティリード(不整合データの読み込み)」や「書き込みの競合(Race Condition)」が発生する。
上記のサンプルコードでは簡略化のために `SyncLock` を用いているが、これは「同一プロセス内のスレッド間」でしか有効ではない。
【解決策】
プロセス間で排他制御を行うには、OSカーネルオブジェクトである `Mutex` (System.Threading.Mutex) を使用する。
‘ 名前付きミューテックスによるプロセス間排他
Dim mutex As New Mutex(False, “Local\EnterpriseCoreDataBusMutex”)
mutex.WaitOne()
Try
‘ — メモリマップトファイルの読み書き —
Finally
mutex.ReleaseMutex()
End Try
※ただし、ミューテックスの取得・解放にはカーネルモードへの遷移(数十〜数百クロック)が発生するため、極限のスピード(数マイクロ秒)を追求する場合は、CAS(Compare-And-Swap)操作を模したアトミックフラグをメモリの先頭に配置し、スピンロック(SpinWait)を自前で実装するという高度な手法が必要になる場合もある。
② ガベージコレクション(GC)の完全排除
前述の通り、マネージドヒープオブジェクトの生成はパフォーマンスを破壊する。
- ループ内で `New` キーワードを使わない。
- 文字列連結(`&` 演算子など)を行わず、必要であれば `Span
` や `Memory `(.NET Core以降)を活用してメモリのアロケーションをゼロにする。 - VB.NETにおいても、パフォーマンスクリティカルなループ内ではオブジェクト指向の美しさを捨て、プリ型とポインタ的操作(あるいは配列の直接操作)に徹すること。
③ プロセス終了時のライフサイクル管理
メモリマップトファイルやミューテックスはOS資源である。アプリケーションが異常終了(クラッシュ)した場合、ハンドルがリークする可能性がある。
`CreateOrOpen` を用いることで再起動時の復旧は担保されるが、設計段階で「誰がリソースのライフサイクル(生成・破棄)の責任を持つのか(通常はマスタープロセス)」を明確にアーキテクチャに組み込んでおく必要がある。
—
結び:レガシーの殻を破る技術的矜持
「VB.NETだから遅い」「業務システムだからこの程度の遅延は許容される」――そんな言い訳は、プロフェッショナルなエンジニアの辞書には存在しない。
VB.NETは、その簡潔な構文の裏側で、C#と全く同じCLR(Common Language Runtime)の恩恵を受けており、Win32 APIやアンマネージドメモリを自在に操るポテンシャルを秘めている。
今回紹介した `MemoryMappedFile` を用いたプロセス間通信は、ファイルI/Oやネットワークスタックの呪縛からシステムを解放し、ハードウェアの限界性能を引き出すための強力な武器となる。既存のレガシーシステムに限界を感じているなら、今すぐこのメカニズムを導入し、圧倒的なパフォーマンスの差を体感してほしい。
