Language: EN

csharp-shared-memory

Sharing Memory in C# with Shared Memory

SharedMemory is an open-source library available on GitHub, created by Justin Stenning, which offers shared memory classes in C#.

Communication and data exchange between processes is a necessity that we sometimes have when developing software. There are different ways, more or less efficient and secure, to share information between processes.

One of the mechanisms is SharedMemory, a mechanism that allows multiple processes to share memory segments in the address space of a computer.

These processes can access and modify the data stored in shared memory efficiently, which is useful for communication and cooperation between independent processes.

However, it is essential to implement proper synchronization and access control to shared memory to avoid race conditions and ensure the coherence of shared data, which can be achieved through the use of semaphores, mutex, or other synchronization mechanisms.

The SharedMemory library in C# offers shared memory classes that facilitate efficient and secure data exchange between processes. It simplifies the process of sharing data and takes care of synchronization and concurrent access.

These classes allow sharing data between processes efficiently and securely:

  • SharedBuffer: An abstract base class that wraps a memory-mapped file, exposing read/write operations and implementing a small header to allow clients to open the shared buffer without knowing the size in advance.
  • BufferWithLocks: An abstract class that extends SharedBuffer to provide simple read/write locking support through the use of EventWaitHandles.
  • SharedArray: A simple implementation of a generic array that uses a shared memory buffer. It inherits from BufferWithLocks to provide support for thread synchronization.
  • BufferReadWrite: Provides read/write access to a shared memory buffer, with several overloads to support reading and writing of structures, copying to and from IntPtr, and more. It inherits from SharedMemory.BufferWithLocks to provide support for thread synchronization.
  • CircularBuffer: Implementation of a lock-free FIFO circular buffer (also known as a ring buffer). With support for 2 or more nodes, this implementation supports multiple readers and writers. The lock-free approach is implemented using Interlocked.Exchange and EventWaitHandles.
  • RpcBuffer: Simple bidirectional RPC channel that uses CircularBuffer. It supports a master/slave pair per channel. Only available in .NET 4.5+ / .NET Standard 2.0.

How to use SharedMemory

We can easily add the library to a .NET project, through the corresponding Nuget package.

Install-Package SharedMemory

Basic Example

Let’s see a practical and simple example of how to use the SharedArray class to share integer information between two processes in C#:

// Process 1: Writer
using (var sharedArray = new SharedArray<int>(100))
{
    // Write data to the shared array
    for (int i = 0; i < sharedArray.Length; i++)
    {
        sharedArray[i] = i;
    }
    
    // Wait for process 2 to complete reading
    while (sharedArray.ReadCount > 0)
    {
        Thread.Sleep(10);
    }
}

// Process 2: Reader
using (var sharedArray = new SharedArray<int>(100))
{
    // Read data from the shared array
    for (int i = 0; i < sharedArray.Length; i++)
    {
        int value = sharedArray[i];
        Console.WriteLine(value);
    }
    
    // Mark the array as read
    sharedArray.MarkRead();
}

In this example, process 1 is the writer and process 2 is the reader. Process 1 writes the numbers from 0 to 99 in the shared array, while process 2 reads and displays the values. The SharedArray class automatically handles synchronization and concurrent access to the shared data.

Here are some examples of how to use SharedMemory extracted from the library’s documentation

SharedArray

Console.WriteLine("SharedMemory.SharedArray:");
using (var producer = new SharedMemory.SharedArray<int>("MySharedArray", 10))
using (var consumer = new SharedMemory.SharedArray<int>("MySharedArray"))
{
    producer[0] = 123;
    producer[producer.Length - 1] = 456;
    
    Console.WriteLine(consumer[0]);
    Console.WriteLine(consumer[consumer.Length - 1]);
}

CircularBuffer

Console.WriteLine("SharedMemory.CircularBuffer:");
using (var producer = new SharedMemory.CircularBuffer(name: "MySharedMemory", nodeCount: 3, nodeBufferSize: 4))
using (var consumer = new SharedMemory.CircularBuffer(name: "MySharedMemory"))
{
    // nodeCount must be one larger than the number
    // of writes that must fit in the buffer at any one time
    producer.Write<int>(new int[] { 123 });
    producer.Write<int>(new int[] { 456 });
   
    int[] data = new int[1];
    consumer.Read<int>(data);
    Console.WriteLine(data[0]);
    consumer.Read<int>(data);
    Console.WriteLine(data[0]);
}

BufferReadWrite

Console.WriteLine("SharedMemory.BufferReadWrite:");
using (var producer = new SharedMemory.BufferReadWrite(name: "MySharedBuffer", bufferSize: 1024))
using (var consumer = new SharedMemory.BufferReadWrite(name: "MySharedBuffer"))
{
    int data = 123;
    producer.Write<int>(ref data);
    data = 456;
    producer.Write<int>(ref data, 1000);
    
    int readData;
    consumer.Read<int>(out readData);
    Console.WriteLine(readData);
    consumer.Read<int>(out readData, 1000);
    Console.WriteLine(readData);
}

RpcBuffer

Console.WriteLine("SharedMemory.RpcBuffer:");
// Ensure a unique channel name
var rpcName = "RpcTest" + Guid.NewGuid().ToString();
var rpcMaster = new RpcBuffer(rpcName);
var rpcSlave = new RpcBuffer(rpcName, (msgId, payload) =>
{
    // Add the two bytes together
    return BitConverter.GetBytes((payload[0] + payload[1]));
});

// Call the remote handler to add 123 and 10
var result = rpcMaster.RemoteRequest(new byte[] { 123, 10 });
Console.WriteLine(result); // outputs 133

SharedMemory is Open Source, and all the code and documentation are available in the project’s repository at https://github.com/justinstenning/SharedMemory