Recommend a Solution for High-Speed Data Exchange Between Processes

Recommend a Solution for High-Speed Data Exchange Between Processes

In actual development, an application may consist of multiple programs, and the data interaction between the components of this application becomes crucial. How can we achieve fast and efficient data interaction?

Last updated 8/3/2025 4:23 PM
老码识途呀
12 min read
Category
Sharing
Tags
.NET C# Shared Memory Interprocess Communication

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:

  1. Creating Shared Memory: There are two ways to create a memory-mapped file. One is to create it directly using the MemoryMappedFile.CreateNew method; the other is to create it from an existing file using MemoryMappedFile.CreateFromFile.
  2. Shared Memory Accessor: In .NET, the MemoryMappedViewAccessor is used to access shared memory, created via the CreateViewAccessor method of a MemoryMappedFile instance.
  3. Read/Write Method: Data is stored as byte arrays in shared memory. You can read and write byte arrays using the accessor's ReadArray and WriteArray methods.

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!

Keep Exploring

Related Reading

More Articles