Within this article I want to show you how an inter-process communication using memory-mapped files may be implemented. Within this first of two articles you will learn the basic concept to transfer a data structure and a data object between two processes. Later on, in the second article, you will see how to implement a thread safe and more flexible data transfer without shared data assemblies.
Memory Mapped Files
The .NET Framework offers non-persisted memory-mapped files which are not associated with a file on a disk. Therefore it is possible to create a shared memory for inter-process communications.
Transfer a data structure
The following data structure is used to transfer some information between the two processes.
public struct MyDataStructure { public Int32 MyValueA { get; set; } public Int32 MyValueB { get; set; } }
The sender has to create a new memory-mapped file. The file must get a unique name. Furthermore the file capacity must be specified. The capacity will always be increased to a multiple of 4 kilo bytes, because that’s the lowest block size which is used by the file management. For example if a capacity value of 6000 is set, a memory-mapped file with a capacity of 8092 bytes will be created.
After creation of the memory-mapped file the data should be written into the file. Therefore a MemoryMappedViewAccessor must be created. This view accessor contains a Write function to write the data structure into the file.
//create file using (MemoryMappedFile file = MemoryMappedFile.CreateNew( "MyMemoryMappedFile", 4096, MemoryMappedFileAccess.ReadWrite)) { using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor()) { MyDataStructure data = new MyDataStructure(); data.MyValueA = 12; data.MyValueB = 20; accessor.Write<MyDataStructure>(0, ref data); } //write info Console.WriteLine("Memory Mapped File created"); //wait Console.ReadKey(); }
The Console.ReadKey() function is used inside of the using-block because the memory mapped file has to stay open until it is read by the other process. To read the data a second console application should be implemented. Within this application you have to open the previously created memory-mapped file. To read the file content the MemoryMappedViewAccessor is used again. This time the view accessor will be created with limited file access because we only want to read data. To read the data a Read function is available. The following source code shows the data receiver console application.
MyDataStructure data = new MyDataStructure(); //create file using (MemoryMappedFile file = MemoryMappedFile.OpenExisting( "MyMemoryMappedFile", MemoryMappedFileRights.Read)) { using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor(0,0, MemoryMappedFileAccess.Read)) { accessor.Read<MyDataStructure>(0, out data); } } //write info Console.WriteLine("Memory Mapped File read"); Console.WriteLine("MyValueA = " + data.MyValueA.ToString()); Console.WriteLine("MyValueB = " + data.MyValueB.ToString()); //wait Console.ReadKey();
Transfer an object
In the previously example a data structure was transferred between the two processes. If you want to use an object to store your data you can’t use the MemoryMappedViewAccessor. Instead the MemoryMappedViewStream must be used.
The following data class is used for the example.
[Serializable] public class MyClass { public string MyValueA { get; set; } public string MyValueB { get; set; } public List<Int32> MyValuesC { get; set; } public List<string> MyValuesD { get; set; } }
The sender will serialize the data object into the view stream by using a BinaryFormatter.
//create file using (MemoryMappedFile file = MemoryMappedFile.CreateNew( "MyMemoryMappedFile", 4096, MemoryMappedFileAccess.ReadWrite)) { using (MemoryMappedViewStream stream = file.CreateViewStream()) { MyClass data = new MyClass(); data.MyValueA = "foobar"; data.MyValueB = "test"; data.MyValuesC = new List<Int32>() { 1, 2, 3 }; data.MyValuesD = new List<string>() { "foo", "bar" }; BinaryFormatter serializer = new BinaryFormatter(); serializer.Serialize(stream, data); stream.Seek(0, SeekOrigin.Begin); } //write info Console.WriteLine("Memory Mapped File created"); //wait Console.ReadKey(); }
The receiver has to deserialize the data.
MyClass data = null; //create file using (MemoryMappedFile file = MemoryMappedFile.OpenExisting( "MyMemoryMappedFile", MemoryMappedFileRights.Read)) { using (MemoryMappedViewStream stream = file.CreateViewStream(0, 0, MemoryMappedFileAccess.Read)) { BinaryFormatter serializer = new BinaryFormatter(); data = serializer.Deserialize(stream) as MyClass; } //write info Console.WriteLine("Memory Mapped File read"); Console.WriteLine("MyValueA = " + data.MyValueA); Console.WriteLine("MyValueB = " + data.MyValueB); foreach (Int32 value in data.MyValuesC) { Console.WriteLine("MyValuesC = " + value.ToString()); } foreach (string value in data.MyValuesD) { Console.WriteLine("MyValuesD = " + value); } //wait Console.ReadKey(); }
The binary formatter has one big disadvantage. The data class to serialize and to deserialize must be available in a shared assembly which is used in both applications. In the next article I will show you another possibility which does not need a shared data assembly to serialize an object.
Summary
By using memory-mapped files it is possible to implement an inter-process communication. But like in all multi-thread scenarios the programmer has to implement a thread-safe access for the shared elements. Therefore the read and write functions must be locked, e.g. by using a Mutex. In the next article I will show you how this thread-safe memory-mapped file may be implemented.
Pingback: Inter-Process Communication with Memory-Mapped Files: Exchange date between a Windows Service and a Desktop Application | coders corner
Pingback: Inter-Process Communication with anonymous pipes, Part 01: transfer a byte stream | coders corner
Sollte wohl 8192 statt 8092 heißen.
Wow,… thank you very much for this article! This (and the next one as well) helped me a lot.