Inter-Process Communication with Memory-Mapped Files, Part 02: serialisation and thread-safe access

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.

Advertisements
Dieser Beitrag wurde unter .NET, C# abgelegt und mit , , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

7 Antworten zu Inter-Process Communication with Memory-Mapped Files, Part 02: serialisation and thread-safe access

  1. Pingback: Inter-Process Communication with Memory-Mapped Files: Exchange date between a Windows Service and a Desktop Application | coders corner

  2. Justin Wittock schreibt:

    Hello. Great article and thank you. Could you please identify the differences and purposes between mutex and lock? Thanks.

    • oliverfunke schreibt:

      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.

  3. Batata schreibt:

    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.

  4. Jan Vasko schreibt:

    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);

  5. costea schreibt:

    I like part-1 more; for me it does the job , and it is rock solid…
    thanks a lot for sharing…

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s