In actual development, an application may consist of multiple programs. The data interaction between these components becomes crucial. How can we achieve fast and efficient data exchange? For inter-process communication across servers, remote procedure call (RPC) technologies such as Remoting, WCF, and gRPC can be used. This approach involves network transmission through the network card, incurring performance overhead from data conversion and network transfer. If the processes are on the same server, using RPC for inter-process data exchange is not optimal. So how can we bypass the network for inter-process data interaction? The answer is "shared memory." Today, we will use a simple example to briefly describe how processes can exchange data via shared memory. This article is intended for learning and sharing; please point out any shortcomings.

What is Shared Memory?
In an operating system, each process is allocated an independent memory space for running programs and storing data. The memories of different processes are isolated from each other to ensure stable and orderly program execution. Although this memory protection mechanism largely guarantees data security and program stability, it also creates barriers between processes that need to interact. Fortunately, operating systems have accounted for this situation with shared memory. Shared Memory is an Inter-Process Communication (IPC) mechanism that allows multiple processes to share the same physical memory, thereby improving data exchange efficiency. Compared to other IPC methods (such as pipes, message queues, etc.), shared memory offers the advantages of high speed and low overhead because data is stored directly in memory without requiring data copying through the kernel.
Shared Memory in .NET
On the .NET platform, shared memory is implemented through MemoryMappedFile. Memory-mapped files allow you to reserve a block of address space and map physical storage to that memory space for operations. Physical storage is managed by files, while memory-mapped files are operating-system-level memory management. The main knowledge points related to memory-mapped file technology are as follows:
- Creating Shared Memory: There are two ways to create a memory-mapped file. One is to create it directly using the
MemoryMappedFile.CreateNewmethod; the other is to create it from an existing file usingMemoryMappedFile.CreateFromFile. - Shared Memory Accessor: In .NET, the
MemoryMappedViewAccessoris used to access shared memory, created via theCreateViewAccessormethod of aMemoryMappedFileinstance. - Read/Write Method: Data is stored as byte arrays in shared memory. You can read and write byte arrays using the accessor's
ReadArrayandWriteArraymethods.
Creating a Memory-Mapped File
There are two ways to create a memory-mapped file:
Method 1: Direct creation using MemoryMappedFile.CreateNew, as shown below:

Method 2: Creation from an existing file using MemoryMappedFile.CreateFromFile, which uses an existing file or file stream, as shown below:

Opening an existing memory-mapped file is done using MemoryMappedFile.OpenExisting, as shown below:

Creating or opening a memory-mapped file is done using MemoryMappedFile.CreateOrOpen, as shown below:

Memory-Mapped File Accessor
The memory-mapped file accessor is used to operate on shared memory. It is created via the CreateViewAccessor method of a MemoryMappedFile instance, as shown below:

Releasing Memory-Mapped File Resources
MemoryMappedFile implements the IDisposable interface, so you can simply call its Dispose method.
Steps to Use Shared Memory
In this example, there are two WinForm executable programs: one writes to shared memory and the other reads from it. Both programs run simultaneously, as shown below:

This demonstrates reading/writing data of a fixed-format struct and variable-length data to shared memory.
Creating a Memory-Mapped File Object
In this example, the memory-mapped file is created when writing to shared memory and opened when reading from shared memory, as shown below:
/// <summary>
/// Create or open shared memory
/// </summary>
public void CreateOrOpenSharedMemory()
{
this.memoryMapped = MemoryMappedFile.CreateOrOpen(this.MapName, this.capacity, MemoryMappedFileAccess.ReadWriteExecute, MemoryMappedFileOptions.None, HandleInheritability.Inheritable);
this.memoryAccessor = this.memoryMapped.CreateViewAccessor();
}
/// <summary>
/// Create shared memory from a file
/// </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>
/// Open existing shared memory
/// </summary>
public void OpenShareMemory()
{
this.memoryMapped = MemoryMappedFile.OpenExisting(this.MapName);
this.memoryAccessor = this.memoryMapped.CreateViewAccessor();
}
Reading and Writing Variable-Length Byte Arrays
For variable-length byte arrays, we use an image opened by the user as an example. The image path and image content are exchanged via shared memory as byte arrays. Their storage format in shared memory is shown below:

Entity Model ImageData
First, create the entity model ImageData, which converts between Bitmap objects and byte arrays, as shown below:
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>
/// Convert object to byte array
/// </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>
/// Convert byte array to object
/// </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();
}
}
}
Reading and Writing Byte Arrays in Shared Memory
Reading and writing variable-length byte arrays in shared memory:
/// <summary>
/// Read byte array
/// </summary>
/// <returns></returns>
public byte[] ReadMemoryWithBytes()
{
byte[] bytes = new byte[this.capacity];
this.memoryAccessor.ReadArray<byte>(0, bytes, 0, bytes.Length);
return bytes;
}
/// <summary>
/// Write byte array
/// </summary>
/// <param name="bytes"></param>
public void WriteMemoryWithBytes(byte[] bytes)
{
this.memoryAccessor.WriteArray<byte>(0, bytes, 0, bytes.Length);
}
Here, capacity is the default capacity of the memory-mapped file, set to 10 MB.
Calling Write to Shared Memory
In the Okcoder.ShareMemory.Writer project, the user selects an image and displays it on the UI:
private void btnBrowser_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Title = "Select an image";
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;
}
}
Then, clicking the "Blip" button encapsulates the ImageData object, converts it to a byte array, and writes it to shared memory:
private void btnWriteMemory_Click(object sender, EventArgs e)
{
// Create ImageData and convert to byte array
ImageData imageData = new ImageData();
imageData.ImageFullPath = this.txtImagePath.Text;
imageData.ImageContent = (Bitmap)this.pictureBox1.Image;
byte[] bytes = imageData.ImageToBytes();
// Create shared memory helper and open shared memory
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.CreateOrOpenSharedMemory();
// Write byte array to shared memory
helper.WriteMemoryWithBytes(bytes);
}
Calling Read from Shared Memory
In the Okcoder.ShareMemory.Reader project, clicking the "Blip" button reads shared memory, converts it to an ImageData object, and displays it on the UI:
private void btnRead_Click(object sender, EventArgs e)
{
// Create shared memory helper and open shared memory
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.OpenShareMemory();
// Read byte array
byte[] bytes = helper.ReadMemoryWithBytes();
ImageData imageData = new ImageData();
// Convert byte array to ImageData object
imageData.BytesToImage(bytes);
// Assign values to UI
this.txtImagePath.Text = imageData.ImageFullPath;
this.pictureBox1.Image = imageData.ImageContent;
this.pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
}
With two clicks, the image selected by the user in one application is transferred to another application via shared memory. Amazing, isn't it?
Reading and Writing Fixed-Length Content
In practice, shared memory supports value-type structs and reference-type byte arrays. The order of properties in the struct corresponds to their order in memory.
Defining the Entity Model
First, define the TestData struct. To allow pointer conversion, use the StructLayout attribute to declare the struct as serializable:
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 bytes
/// </summary>
public int Id;
/// <summary>
/// Age, 4 bytes
/// </summary>
public int Age;
/// <summary>
/// Scores, 10 elements
/// </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)}";
}
}
}
Writing a Struct Type
First, allocate memory via Marshal.AllocHGlobal, store the struct in the allocated memory via Marshal.StructureToPtr, point the pointer to the start, copy the memory pointed to by the pointer into a byte array via Marshal.Copy, write it to shared memory, and finally free the allocated pointer via Marshal.FreeHGlobal:
/// <summary>
/// Write struct
/// </summary>
/// <param name="data"></param>
public void WriteMemoryWithStruct(TestData data)
{
// Get the size of the struct
int len = Marshal.SizeOf(typeof(TestData));
byte[] bytes = new byte[len];
// Allocate memory and get pointer
IntPtr p = Marshal.AllocHGlobal(len);
// Write struct to memory
Marshal.StructureToPtr(data, p, false);
// Copy memory to array
Marshal.Copy(p, bytes, 0, len);
// Write array to shared memory
this.memoryAccessor.WriteArray<byte>(0, bytes, 0, bytes.Length);
// Free memory
Marshal.FreeHGlobal(p);
// Clear pointer
p = IntPtr.Zero;
}
Reading a Struct Type
First, allocate memory via Marshal.AllocHGlobal, read the byte array from shared memory and copy it to the allocated memory, point the pointer to the start, convert the pointer to a struct via Marshal.PtrToStructure, and finally free the allocated pointer via Marshal.FreeHGlobal:
/// <summary>
/// Read struct
/// </summary>
/// <returns></returns>
public TestData ReadMemoryWithStruct()
{
// Get the size of the struct type
int len = Marshal.SizeOf(typeof(TestData));
byte[] bytes = new byte[len];
// Allocate memory
IntPtr p = Marshal.AllocHGlobal(len);
// Read data from shared memory
this.memoryAccessor.ReadArray<byte>(0, bytes, 0, bytes.Length);
// Copy byte array to pointer
Marshal.Copy(bytes, 0, p, len);
// Convert pointer to struct
TestData data = (TestData)Marshal.PtrToStructure(p, typeof(TestData));
// Free memory
Marshal.FreeHGlobal(p);
// Clear pointer
p = IntPtr.Zero;
return data;
}
Calling Write to Shared Memory
In the Okcoder.ShareMemory.Writer project, clicking the "Struct" button creates a TestData instance, assigns values, and writes it to shared memory:
private void btnWriteStruct_Click(object sender, EventArgs e)
{
// Create TestData and assign values
TestData testData = new TestData();
testData.Id = 100;
testData.Age = 20;
for (int i = 0; i < 10; i++)
{
testData.Scores[i] = i + 60;
}
// Create shared memory helper and open shared memory
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.CreateOrOpenSharedMemory();
// Write struct to shared memory
helper.WriteMemoryWithStruct(testData);
}
Calling Read from Shared Memory
In the Okcoder.ShareMemory.Reader project, clicking the "Struct" button reads the struct from shared memory and displays it in a message box:
private void btnReadStruct_Click(object sender, EventArgs e)
{
// Create shared memory helper and open shared memory
ShareMemoryHelper helper = new ShareMemoryHelper();
helper.OpenShareMemory();
// Read struct
TestData testData = helper.ReadMemoryWithStruct();
MessageBox.Show(testData.ToString());
}
Demonstration
First, exchanging variable-length image data between processes:

Exchanging fixed-length struct data between processes:

That's all for "Recommend a Solution for High-Speed Data Exchange Between Processes". It's intended to spark discussion and shared learning. Let's grow together!
