This is the second of two articles about inter-process communication with memory-mapped files. In this first article you have seen how memory-mapped files may be used to transfer data between several processes. This first implementation had two disadvantages. The binary serialization required a shared data assembly and the file access commands were not thread-safe.
In this second article I want to show you how to implement an inter process-communication which is thread-safe and which does not need a shared data assembly
Serialization with XmlSerializer
The following data class is used to transfer information between the different processes.
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; } }
By using the XmlSerializer the data object will be serialized into XML format and written into s string object. Afterwards the string will be converted to a byte array, because this byte array may be easily read and write to the memory-mapped file by using the MemoryMappedViewAccessor class. The following source code shows how the data object will be converted into an XML formatted string which will be converted to a byte array and written to the memory-mapped file.
static void Main(string[] args) { //create file using (MemoryMappedFile file = MemoryMappedFile.CreateNew( "MyMemoryMappedFile", 4096, MemoryMappedFileAccess.ReadWrite)) { using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor()) { MyClass data = new MyClass(); data.MyValueA = "foobar"; data.MyValueB = null; data.MyValuesC = new List<Int32>() { 1, 2, 3 }; data.MyValuesD = new List<string>() { "foo", "bar" }; string xmlData = SerializeToXml(data); byte[] buffer = ConvertStringToByteArray(xmlData); accessor.WriteArray<byte>(0, buffer, 0, buffer.Length); } //write info Console.WriteLine("Memory Mapped File created"); //wait Console.ReadKey(); } } public static string SerializeToXml(MyClass data) { string xmlData = null; StringWriter stringWriter = null; XmlSerializer serializer = new XmlSerializer(data.GetType()); stringWriter = new StringWriter(); serializer.Serialize(stringWriter, data); xmlData = stringWriter.ToString(); stringWriter.Close(); return xmlData; } private static byte[] ConvertStringToByteArray(string value) { System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding(); return encoding.GetBytes(value); }
The receiver of the data has to read the byte array and convert it back into a string. Afterwards this string is deserialized into a data class. This data class may be implemented within the assembly of the receiver application. Therefore it is possible to use a shared assembly which contains the data class or each application may implement an own data class, e.g. according to a defined interface. The following source code shows the application which reads the data from the memory-mapped file.
static void Main(string[] args) { MyClass data = null; //read file using (MemoryMappedFile file = MemoryMappedFile.OpenExisting( "MyMemoryMappedFile", MemoryMappedFileRights.Read)) { using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) { //read byte[] buffer = new byte[accessor.Capacity]; accessor.ReadArray<byte>(0, buffer, 0, buffer.Length); //convert to object string xmlData = ConvertByteArrayToString(buffer); data = DeserializeFromXML(xmlData); } } //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(); } public static MyClass DeserializeFromXML(string xmlData) { MyClass data = null; StringReader stringReader = null; XmlSerializer deserializer = new XmlSerializer(typeof(MyClass)); stringReader = new StringReader(xmlData); data = (MyClass)deserializer.Deserialize(stringReader); stringReader.Close(); return data; } public static string ConvertByteArrayToString(byte[] values) { System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding(); return encoding.GetString(values); }
Thread-safety
At the moment the example source code is not thread-safe. Different applications may read and write data to or from the memory-mapped file at the same time. To ensure a thread-safe file access a Mutex should be used.
Within the next example I use a data structure which doesn’t need the serialization steps. This will reduce the number of lines of the examples source code. But the same thread-safe implementation may be used to transfer a data object by using the XmlSerializer. The following data structure is used.
public struct MyDataStructure { public Int32 MyValueA { get; set; } public Int32 MyValueB { get; set; } }
The following source code contains the application of the sender which writes the data into the memory-mapped file. The write command will be locked by using a Mutex. The Console.ReadKey() function is used inside the using-block because the memory mapped file has to stay open until we have read it in the other process.
static void Main(string[] args) { bool mutexCreated; Mutex mutex; //create mutex mutex = new Mutex(false, "MyMutex", out mutexCreated); if (mutexCreated == false) { Console.WriteLine("Mutex error"); Console.ReadKey(); return; } //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; mutex.WaitOne(); accessor.Write<MyDataStructure>(0, ref data); mutex.ReleaseMutex(); } //write info Console.WriteLine("Memory Mapped File created"); //wait Console.ReadKey(); } //close mutex mutex.Close(); }
Within a second application we want to read the date from the memory-mapped file. This time the receiver application will be implemented in a way that it may be started in front of the sender application. The receiver application should execute an endless loop until there is some data to read. The access to the Mutex and the access to the memory-mapped file must be executed in a try-catch-block, because they will not be available until the sender application is started. If the Mutex object is not created a WaitHandleCannotBeOpenedException is thrown and if the memory-mapped file is not available a FileNotFoundException is thrown.
static void Main(string[] args) { Nullable<MyDataStructure> data = null; //get data do { data = GetData(); if (data == null) { Console.Write("."); Thread.Sleep(TimeSpan.FromMilliseconds(500)); } } while (data == null); //write info Console.WriteLine(""); Console.WriteLine("Memory Mapped File read"); Console.WriteLine("MyValueA = " + data.Value.MyValueA.ToString()); Console.WriteLine("MyValueB = " + data.Value.MyValueB.ToString()); //wait Console.ReadKey(); } static Nullable<MyDataStructure> GetData() { MyDataStructure data = new MyDataStructure(); //create file try { Mutex mutex = Mutex.OpenExisting("MyMutex"); mutex.WaitOne(); using (MemoryMappedFile file = MemoryMappedFile.OpenExisting( "MyMemoryMappedFile", MemoryMappedFileRights.Read)) { using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) { accessor.Read<MyDataStructure>(0, out data); } } } catch (FileNotFoundException) { return null; } catch (WaitHandleCannotBeOpenedException) { return null; } //return return data; }
Summary
By using the XmlSerializer and the Mutex object it is possible to implement an inter-process communication with memory-mapped files which is thread-safe and allows independent implementation of the different applications without the need of shared assemblies.
Pingback: Inter-Process Communication with Memory-Mapped Files: Exchange date between a Windows Service and a Desktop Application | coders corner
Hello. Great article and thank you. Could you please identify the differences and purposes between mutex and lock? Thanks.
Hi justin,
In general, lock and mutex may be used to implement a thread-safe access to a statement block or a shared resource. Lock is used for threads of the same process and mutex allows the synchronisation of threads of different processes. Therefore the lock statement cannot be used for a interprocess synchronization.
Hi and thanks,
You mentioned that binary serialization requires a shared assembly between the sender and the receive, and that is why it is problematic. On the other hand you propose to convert the object to an xml string and further to bytes. The receiver gets bytes and converts bytes to xml string. the typecast data = (MyClass)deserializer.Deserialize(stringReader); still requires a shared assembly. To avoid the assembly you need to parse the received string. To parse the received string you need to know what is in it. To look what is in that string you need to look at sample xml serialized instances of the shared class. It is just another type of dependency, the sender and receiver need to have a contract of what they send and receive from each others, a shared assembly or a shared xsd do exactly that.
Was totally stuck until I read this, now back up and rugnnin.
Hi.
Because of variable length of data, I propose:
private byte[] ConvertStringToByteArray(string value, int max_length)
{
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] data = new byte[max_length];
int i = encoding.GetBytes(value, 0, value.Length, data, 0);
return data;
}
And calling will look like this:
byte[] buffer = ConvertStringToByteArray(xmlData, (int)accessor.Capacity);
I like part-1 more; for me it does the job , and it is rock solid…
thanks a lot for sharing…
Converting this to WinForms caused the sender app to hang. Adding a mutex.ReleaseMutex() inside the GetData() stopped this from happening. Because I am using a ViewStream into a byte[] instead of a ViewAccesor, data was never null. So I was collecting the data on a Timer.Tick() instead of the do(){} loop.
The most important part though, is this is the first example I have been able to get my head around and actually get working. So thank you for providing something that is user-friendly to those like myself, who find it easy to get bogged down in overly-complex, jargon-heavy examples.
Of course, my solution to the problem could be wrong, so it could all fall apart yet… we shall see.
Hello,
thank you very much for this great article. I was able to follow along with minimal prior programming experience, and learned a ton of stuff in the process. Awesome!
And just like „Mr Janus“, I also scratched my head over the missing ReleaseMutex() call in GetData() 🙂