{"id":78283,"date":"2018-04-17T13:58:02","date_gmt":"2018-04-17T13:58:02","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=78283"},"modified":"2021-06-03T16:46:57","modified_gmt":"2021-06-03T16:46:57","slug":"sharing-caring-using-memory-mapped-files-net","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/development\/dotnet-development\/sharing-caring-using-memory-mapped-files-net\/","title":{"rendered":"Sharing is Caring: Using Memory Mapped Files in .NET"},"content":{"rendered":"<p><a id=\"post-78283-_gjdgxs\"><\/a> Creating large complex objects exacts a toll on computing resources. When these objects can be shared, skipping their recreation becomes an enviable performance goal. Over the years, many solutions have come to the fore for caching objects. When all the consumers reside on the same physical machine, a not so well-known option, .NET\u2019s <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.io.memorymappedfiles.memorymappedfile?view=netcore-2.0\"><em>MemoryMappedFile<\/em><\/a>, may deliver a performance boon.<\/p>\n<p>This article discusses a few <em>MemoryMappedFile<\/em> concepts as well as implements a simple caching application using it.<\/p>\n<h2>Cache Concerns<\/h2>\n<p>Caching objects for multiple concerns is not a new idea. The goals are simple: avoid recreating an object and ensure it can be shared. Several well-known caching solutions exist, such as <a href=\"https:\/\/memcached.org\/\">memcached<\/a> and <a href=\"https:\/\/redis.io\/\">redis<\/a>, that accomplish these objectives. They also suffer from similar performance challenges \u2013 serialization and network throughput.<\/p>\n<p>Serializing and deserializing objects into a generic format amendable to most caching technologies, such as <a href=\"http:\/\/bsonspec.org\/\">BSON<\/a>, <a href=\"https:\/\/www.json.org\/\">JSON<\/a> or <a href=\"https:\/\/en.wikipedia.org\/wiki\/XML\">XML<\/a>, consume considerable time and computing resources. Passing the properly formatted objects between nodes requires bandwidth and time.<\/p>\n<p>What if your caching needs are local to one node? For example, imagine a server hosting several web applications and Windows Services using the same catalog object. Why not just create the catalog once and share it via files or <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/aa365574(v=vs.85).aspx\">Interprocess Communications<\/a> (IPC)? You then minimize the impact of the more common caching performance culprits.<\/p>\n<p>It turns out that .NET provides the required magic for constructing a local, high-performance cache which this article will now explore.<\/p>\n<h2>Memory-Mapped Files<\/h2>\n<p>Memory-mapped files are not new. For over 20 years, the Windows operating system allowed applications to map virtual addresses directly to a file on disk thereby allowing multiple processes to share it. File-based data looked and, more importantly, performed like system virtual memory. There was another benefit. Memory-mapped files allowed applications to work with objects potentially exceeding their working memory limits.<\/p>\n<p>For much of their history, memory-mapped files suffered from a problem: they required unmanaged code. .NET 4.5 changed that; the new <strong>System.IO.MemoryMappedFiles<\/strong> namespace simplified mapping of files to an application\u2019s logical address space. Maybe even more astonishingly, it did so with only a few significant classes.<\/p>\n<ul>\n<li><strong>MemoryMappedFile<\/strong> \u2013 representation of a memory-mapped file<\/li>\n<li><strong>MemoryMappedFileSecurity<\/strong> \u2013 permissions for a memory-mapped file<\/li>\n<li><strong>MemoryMappedViewAccessor<\/strong> \u2013 randomly accessed view of a memory-mapped file<\/li>\n<li><strong>MemoryMappedViewStream<\/strong> \u2013 representation of a sequentially accessed stream (alternate to memory-mapped files)<\/li>\n<\/ul>\n<p>Combining the performance needs of a local caching mechanism with the capabilities of memory-mapped files promises an auspicious marriage. The next discussion explores one such solution.<\/p>\n<h2>Basic Implementation<\/h2>\n<p>This demonstration revolves around one generic class, <strong>MemoryMap<\/strong>. It supports a cache that allows for creating and loading a serializable object. Its public face contains a few read-only properties and three public methods to achieve this end. The public methods are<\/p>\n<ul>\n<li><strong>MemoryMap<\/strong> \u2013 constructor with a string parameter that serves as instance identifier<\/li>\n<li><strong>Create<\/strong> \u2013 creates the memory-mapped file with the provide object data<\/li>\n<li><strong>Load<\/strong> \u2013 returns the object stored in the underlying memory-mapped file<\/li>\n<\/ul>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"228\" height=\"361\" class=\"wp-image-78284\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-160.png\" \/><\/p>\n<h3>Solution Overview<\/h3>\n<p>The Visual Studio 2017 solution, <em>SimpleTalkMemoryMapDemo.sln<\/em>, contains three .NET Core 2.0 projects as shown below. The source code is available on <a href=\"https:\/\/github.com\/simpletalkdemos\/MemoryMap\">GitHub<\/a>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"304\" height=\"222\" class=\"wp-image-78285\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-161.png\" \/><\/p>\n<p>There are three projects in the solution:<\/p>\n<ul>\n<li><a id=\"post-78283-_30j0zll\"><\/a><em>DemoCache<\/em> \u2013 class library containing the earlier noted <strong>MemoryMap<\/strong> class along with two plain old CLR object (POCO) classes, <strong>BigDataChild<\/strong> and <strong>BigDataParent<\/strong>, for sharing.<\/li>\n<li><em>Producer<\/em> \u2013 console project referencing <strong>DemoCache<\/strong> which creates, loads and reads a <strong>BigDataParent<\/strong>.<\/li>\n<li><em>Consumer<\/em> \u2013 console project referencing <strong>DemoCache<\/strong> which loads an instance of <strong>BigDataParent<\/strong>.<\/li>\n<\/ul>\n<p>NOTE: To improve code readability, <strong>using<\/strong> statements were omitted and full class names skipped. The source code includes the required <strong>using<\/strong> statements.<\/p>\n<h3>MemoryMap<\/h3>\n<p><a id=\"post-78283-_1fob9te\"><\/a> Leveraging the memory mapped file cache begins with instantiating an instance of <strong>MemoryMap<\/strong>. The required <strong>memoryMapName<\/strong> parameter serves several functions. It defines the <strong>MemoryMappedFile<\/strong> instance along with the supporting <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/threading\/mutexes\">Mutex<\/a> and file.<\/p>\n<pre class=\"lang:c# theme:vs2012\">public string MemoryMapName { get; }\r\npublic string MemoryMapFileDirectory { get; }\r\npublic string MemoryMapFilePath { get; }\r\npublic string MutexName { get; }\r\npublic MemoryMap(string memoryMapName)\r\n{\r\n    Validate(memoryMapName);\r\n    MemoryMapName = memoryMapName;\r\n    MutexName = $\"{MemoryMapName}-Mutex\";\r\n    MemoryMapFileDirectory = \"c:\\\\temp\";\r\n    MemoryMapFilePath = Path.Combine(\r\n        MemoryMapFileDirectory, memoryMapName);\r\n}<\/pre>\n<p>Once instantiated, working with a <strong>MemoryMappedFile<\/strong> begins with <strong>Create<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">public void Create(T data)\r\n{\r\n    if (!data.GetType().IsSerializable)\r\n        throw new ArgumentException(\r\n            \"Type is not serializable.\", nameof(data));<\/pre>\n<p>It\u2019s important to remember that the <strong>data<\/strong> parameter must be serializable. Otherwise, <strong>Create<\/strong> will very quickly inform you via an exception. The next line calls upon the insightfully named private method <strong>Serialize<\/strong> to convert the <strong>data<\/strong> into bytes. This and the other helper methods will be explored later on in the discussion.<\/p>\n<pre class=\"lang:c# theme:vs2012 \">  var objectBytes = Serialize(data);<\/pre>\n<p>Outputting number of bytes <strong>data<\/strong> consumed was not required, but aren\u2019t you curious?<\/p>\n<pre class=\"lang:c# theme:vs2012\">  Console.WriteLine(\r\n        $\"Save data's byte count: {objectBytes.Length.ToString(\"N0\")}\");<\/pre>\n<p>Since the underlying <strong>MemoryMappedFile<\/strong> object could be accessed by different threads and processes, the best way to minimize pitfalls is by restricting access via an interprocess synchronization primitive. <strong>GetMutex<\/strong>, another helper, does the work.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var mutex = GetMutex(MutexName);<\/pre>\n<p>Some prophylaxis is required before enlisting the <strong>MemoryMappedFile<\/strong>\u2019s underlying physical file.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  PrepareMemoryMapFile(MemoryMapFileDirectory, MemoryMapFilePath);<\/pre>\n<p>Finally, the moment has arrived \u2013 the actual creation of the <strong>MemoryMappedFile<\/strong>. It is worth noting that the below implementation is only one of many possible implementations. Whatever the implementation, though, a <strong>MemoryMappedViewStream<\/strong> is required to persist bytes.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(\r\n        MemoryMapFilePath, FileMode.CreateNew, \r\n        MemoryMapName, objectBytes.Length))\r\n    {\r\n        using (var memoryMappedViewStream = memoryMappedFile.CreateViewStream())\r\n        {\r\n            memoryMappedViewStream.Write(objectBytes, 0, objectBytes.Length);\r\n        }\r\n    }\r\n    mutex.ReleaseMutex();\r\n}<\/pre>\n<p>Accessing a <strong>MemoryMappedFile<\/strong> instance\u2019s data constitutes is the job of <strong>Load<\/strong>. Unsurprisingly, it parallels <strong>Create<\/strong> except it\u2019s now reading bytes and focused on deserializing them.<\/p>\n<pre class=\"lang:c# theme:vs2012\">public T Load()\r\n{\r\n    using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(\r\n        MemoryMapFilePath, FileMode.Open, MemoryMapName))\r\n    {\r\n        T obj;\r\n        var mutex = GetMutex(MutexName);\r\n          \r\n        using (var memoryMappedViewStream = memoryMappedFile.CreateViewStream())\r\n        {\r\n            var binaryReader = new BinaryReader(memoryMappedViewStream);<\/pre>\n<p>Two helpers, <strong>ReadAll<\/strong> and <strong>Deserialize<\/strong>, convert the byte array read from the <strong>memoryMappedViewStream<\/strong> object with a .NET <strong>BinaryReader<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">          obj = Deserialize(ReadAll(binaryReader));\r\n        }\r\n         \r\n        mutex.ReleaseMutex();\r\n        return obj;\r\n    }\r\n}<\/pre>\n<p>Before leaving the discussion of the core <strong>MemoryMap<\/strong> methods, it\u2019s nice to know that a file-based store is not the only option for a cache. <strong>System.IO.MemoryMappedFiles<\/strong> also supports a memory-based store instead of physical files. This alternative, <strong>MemoryMappedViewStream<\/strong>, which is not explored in this article, offers its own pluses and minuses. For example, while faster, it is comparatively limited in size and requires an active process to keep it alive.<\/p>\n<p>Time to consider the lowly helpers facilitating <strong>Create<\/strong> and <strong>Load<\/strong>.<\/p>\n<h4>Validate<\/h4>\n<p><strong>MemoryMap<\/strong> leans heavily on the <strong>memoryMapName<\/strong> parameter with which it is instantiated. For example, <strong>MemoryMappedFile<\/strong> objects demand a physical file and it\u2019s included in the path. Therefore, the <strong>Validate<\/strong> method tries to safeguard that <strong>memoryMapName<\/strong> will satisfy Windows\u2019 file naming expectations.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static void Validate(string memoryMapName)\r\n{\r\n    if (string.IsNullOrWhiteSpace(memoryMapName))\r\n        throw new ArgumentNullException(nameof(memoryMapName));\r\n    if (memoryMapName.IndexOfAny(Path.GetInvalidPathChars()) &gt; 0)\r\n        throw new ArgumentException(\r\n            $\"{memoryMapName} contains invalid characters.\");\r\n}<\/pre>\n<p>The importance of <strong>memoryMapName<\/strong> goes beyond file naming. As you will soon see, <strong>MemoryMap<\/strong> uses it for managing locks and accessing <strong>MemoryMappedFile<\/strong> instances.<\/p>\n<h4>File Hygiene<\/h4>\n<p>Since <strong>MemoryMap<\/strong> saves its data to a physical file, it is important to ensure it can do so without issue. That translates into checking that a directory exists, and the file does not.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static void PrepareMemoryMapFile(\r\n    string fileDirectory, string filePath)\r\n{\r\n    if (!Directory.Exists(fileDirectory))\r\n        Directory.CreateDirectory(fileDirectory);\r\n    if (File.Exists(filePath))\r\n        File.Delete(filePath);\r\n}<\/pre>\n<p>The first check ensures that <em>C:\/temp<\/em> exists. The second deletes any preexisting version of the physical file with the same name.<\/p>\n<h4>Managing Contention<\/h4>\n<p>Avoiding conflict and corruption with multiple data readers and writers demand attention. This application leverages Mutexes in a fashion some readers might find worthwhile, even in a production implementation.<\/p>\n<p>Two points merit mention. The use of the perennial C# favorite <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/language-reference\/keywords\/lock-statement\"><strong>lock<\/strong><\/a> keyword is not adequate. It only handles multiple threads within the same process. <strong>MemoryMap<\/strong> must manage sharing conflicts between multiple processes on the server. Naming the mutex instance is the other key idea. Doing so exposes it to all processes on the operating system.<\/p>\n<p>Warning: Mutex naming demands thoughtful consideration. For example, if names aren\u2019t unique one mutex could unintentionally lock unrelated resources.<\/p>\n<p><strong>GetMutex<\/strong> helper handles the creation of a mutex. For our purposes, that only occurs when loading and retrieving data. If other avenues to the data existed, such as, update and delete methods, the same basic logic should suffice.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private Mutex GetMutex(string mutexName)\r\n{<\/pre>\n<p>The first condition handles the possibility that there may NOT be a mutex. In both cases though, the code locks via <strong>WaitOne<\/strong> until the desired named mutex becomes available.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  if (Mutex.TryOpenExisting(MutexName, out Mutex mutex))\r\n    {\r\n        mutex.WaitOne();\r\n    }\r\n    else\r\n    {\r\n        mutex = new Mutex(true, MutexName, out bool mutexCreated);\r\n        if (!mutexCreated)\r\n            mutex.WaitOne();\r\n    }\r\n    return mutex;\r\n}<\/pre>\n<p>Waiting for a lock to clear is not without drawbacks. I doubt a production ready caching solution will find that very satisfying, a subject discussed later in the article.<\/p>\n<h4>Reading &amp; Writing Data<\/h4>\n<p>When working thru <strong>MemoryMappedFile<\/strong> mechanics, it is easy to forget the importance of reading and writing the data. Any solution\u2019s success hinges on its implementation. This one, once again, takes a simple, albeit understandable tack.<\/p>\n<p>First, <strong>MemoryMap<\/strong> converts a serializable object of type T to an array of bytes for writing to a file. <strong>Serialize<\/strong> relies upon .NET\u2019s <strong>BinaryFormatter<\/strong> as shown below. Such ease of use comes at a price, though. For example, it\u2019s limited to about 6 MB of bytes; acceptable for demonstration purposes not so much for production.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static byte[] Serialize(T obj)\r\n{\r\n    using (var memoryStream = new MemoryStream())\r\n    {\r\n        var binaryFormatter = new BinaryFormatter();\r\n        binaryFormatter.Serialize(memoryStream, obj);\r\n        return memoryStream.ToArray();\r\n    }\r\n}<\/pre>\n<p>Reading the data from <strong>MemoryMappedFile<\/strong> requires two helpers. <strong>ReadAll<\/strong> obtains the object\u2019s raw bytes in chunks from the .NET <strong>BinaryReader<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static byte[] ReadAll(BinaryReader binaryReader)\r\n{\r\n    const int bufferSize = 4096;\r\n    using (var memoryStream = new MemoryStream())\r\n    {\r\n        byte[] buffer = new byte[bufferSize];\r\n        int count;\r\n        while ((count = binaryReader.Read(buffer, 0, buffer.Length)) != 0)\r\n            memoryStream.Write(buffer, 0, count);\r\n        return memoryStream.ToArray();\r\n    }\r\n} <\/pre>\n<p><strong>Deserialize<\/strong> mirrors <strong>Serialize<\/strong>. Except this time, it converts a byte array to an object of type <strong>T<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static T Deserialize(byte[] data)\r\n{\r\n    using (var memoryStream = new MemoryStream(data))\r\n    {\r\n        var binaryFormatter = new BinaryFormatter();\r\n        return binaryFormatter.Deserialize(memoryStream) as T;\r\n    }\r\n}<\/pre>\n<h2>Trying It Out<\/h2>\n<p>You can experiment with the demonstration caching solution via two console applications. The first, <em>Producer<\/em>, runs a complete use case of creating test data, <strong>BigDataParent<\/strong>, loading it into cache, and then reading it from cache. The second, <em>Consumer<\/em>, assumes the data already exists and only loads it.<\/p>\n<p>The <em>Producer<\/em> project contains the code creating the test data via the <strong>CreateBigDataParent<\/strong> helper as shown below.<\/p>\n<pre class=\"lang:c# theme:vs2012\">private static BigDataParent CreateBigDataParent(int count)\r\n{\r\n    var random = new Random();<\/pre>\n<p>Admittedly, what adding randomized <strong>BigDataChild<\/strong> objects lacks in realism, it hopefully makes up in demonstration value. Readers may find experimenting with <strong>BigDataChild <\/strong>and <strong>BigDataParent <\/strong>an easy way to test out their changes to <strong>MemoryMap<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var bigDataChildList = new List&lt;BigDataChild&gt;();\r\n    for (var i = 0; i &lt; count; i++)\r\n        bigDataChildList.Add(\r\n            new BigDataChild {\r\n                Id = random.Next(0, 100),\r\n                SomeDouble = random.NextDouble(),\r\n                SomeString = string.Empty.PadLeft(random.Next(1,1000), 'x') }\r\n        );\r\n    return new BigDataParent {\r\n        Description = $\"BigDataParent with {count} BigDataChild\",\r\n        BigDataChildren = bigDataChildList };        \r\n}<\/pre>\n<h3>One Process<\/h3>\n<p>The solution allows for testing <strong>MemoryMap<\/strong> within a single process by setting the <em>Producer<\/em> project to the solution\u2019s startup as shown below.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"363\" height=\"662\" class=\"wp-image-78286\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-162.png\" \/><\/p>\n<p>Clicking the debug button or F5 should produce output similar to that shown below. Exiting debug requires clicking any key in the console.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"379\" height=\"131\" class=\"wp-image-78287\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-163.png\" \/><\/p>\n<p>Inspecting the output suggests that the handiwork was not in vain. Not only do the \u2018in\u2019 and \u2018out\u2019 objects match based on the total of <strong>SomeDouble<\/strong> values (53.2937452901591) for children with the same id, but caching required less than a second to handle 5 megabytes of data.<\/p>\n<p>The code creating the above resides in the <em>Program.cs<\/em> <strong>Main<\/strong> method.<\/p>\n<pre class=\"lang:c# theme:vs2012\">static void Main()\r\n{<\/pre>\n<p>The demonstration begins with creating a test object for loading into <strong>MemoryMap<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var sampleBigDataIn = CreateBigDataParent(10000);\r\n    Console.WriteLine(\r\n        $\"Created '{sampleBigDataIn.Description}'\");            \r\n    Console.WriteLine($\"with a total SomeDouble: \r\n {sampleBigDataIn.BigDataChildren.Sum(x =&gt; x.SomeDouble)}\");<\/pre>\n<p>Since performance drives this effort, it seems like a good idea to check timing to see how long it takes to manage <strong>sampleBigDataIn<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var stopWatch = new Stopwatch();\r\n    stopWatch.Start();<\/pre>\n<p>As most readers probably guessed from the earlier discussion, the <strong>SomeKey<\/strong> parameter for the <strong>MemoryMap<\/strong> constructor uniquely defines it in this server. It literally serves as the key to this specific instance of a <strong>BigDataParent<\/strong> object.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var memoryMap = new MemoryMap&lt;BigDataParent&gt;(\"SomeKey\");\r\n    memoryMap.Create(sampleBigDataIn);\r\n    Console.WriteLine(\r\n        $\"memoryMap.Save elapsed time: {stopWatch.Elapsed}\");<\/pre>\n<p>In a real-world application, code located elsewhere requiring the test object would be executing at this point, but, for the purposes of this demo, just retrieving <strong>sampleBigDataOut<\/strong> now plays best for the demonstration.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var sampleBigDataOut = memoryMap.Load();\r\n    Console.WriteLine($\"memoryMap.Load elapsed time: {stopWatch.Elapsed}\");\r\n    stopWatch.Stop();<\/pre>\n<p>The final several lines serve to help prove what went into <strong>MemoryMap&lt;BigDataParent&gt;(&#8220;SomeKey&#8221;)<\/strong> came out.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var childId = sampleBigDataIn.BigDataChildren[0].Id;\r\n    Console.WriteLine($\"BigDataParent comparison check for childId: {childId}\");\r\n    var totalSomeDoubleIn = sampleBigDataIn.BigDataChildren\r\n        .Where(x =&gt; x.Id.Equals(childId)).Sum(x =&gt; x.SomeDouble);\r\n    Console.WriteLine($\"totalSomeDoubleIn : {totalSomeDoubleIn}\");\r\n    var totalSomeDoubleOut = sampleBigDataOut.BigDataChildren\r\n        .Where(x =&gt; x.Id.Equals(childId)).Sum(x =&gt; x.SomeDouble);           \r\n    Console.WriteLine($\"totalSomeDoubleOut: {totalSomeDoubleOut}\");<\/pre>\n<p>Finally, exit <strong>Main<\/strong> from the console window.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  Console.ReadKey();\r\n}<\/pre>\n<p>The next scenario simulates how another process might access the instance of <strong>BigDataParent<\/strong> just created.<\/p>\n<h3>Two Processes<\/h3>\n<p>After running <em>Producer<\/em>, switching the startup project to the <em>Consumer<\/em> project and executing it simulates accessing the cached a <strong>BigDataParent<\/strong> object from a second process. The output of the <em>Consumer<\/em> project\u2019s <strong>Main<\/strong> method allows for a simple check that you have in fact loaded the correct data.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"369\" height=\"50\" class=\"wp-image-78288\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-164.png\" \/><\/p>\n<p>Comparing the <em>Producer<\/em> and <em>Consumer<\/em> output total <strong>SomeDouble<\/strong> values of 5013.90905729819 suggest the projects are sharing the same instance of <strong>BigDataParent<\/strong>.<\/p>\n<pre class=\"lang:c# theme:vs2012\">static void Main()\r\n{\r\n    var stopWatch = new Stopwatch();\r\n    stopWatch.Start();<\/pre>\n<p>Again, how you instantiate <strong>MemoryMap<\/strong> matters. The text <strong>SomeKey<\/strong> uniquely defines the instance.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  var memoryMap = new MemoryMap&lt;BigDataParent&gt;(\"SomeKey\");\r\n    var sampleBigData = memoryMap.Load();\r\n    Console.WriteLine(\r\n        $\"memoryMap.Load elapsed time: {stopWatch.Elapsed}\");\r\n    Console.WriteLine(\r\n        $\"Loaded '{sampleBigData.Description}'\");\r\n    Console.WriteLine(\r\n        $\"Total SomeDouble: {sampleBigData.BigDataChildren.Sum(x =&gt; x.SomeDouble)}\");<\/pre>\n<p>As before, exit <strong>Main<\/strong> from the console window.<\/p>\n<pre class=\"lang:c# theme:vs2012\">  Console.ReadKey();\r\n}<\/pre>\n<p>Before declaring this two-process demonstration complete, discerning readers may rightfully claim that they were not concurrent. To them, I\u2019d recommend experimentation with running simultaneously multiple instances of the demonstration solution.<\/p>\n<h2>Going Forward<\/h2>\n<p>Crafting a local custom memory mapped file-based caching solution demands vigilance. Constantly asking oneself with each feature whether or not an existing, full featured solution constitutes a better investment of developer time is time well spent. With that warning in mind, several <strong>MemoryMap<\/strong> enhancements seem likely for different production use cases.<\/p>\n<h3>Updates<\/h3>\n<p>As currently coded, <strong>MemoryMap<\/strong> equates to a read-only cache. That shortfall could be easily addressed by adding an update method. The below method signatures suggest a few possible implementations.<\/p>\n<pre class=\"lang:c# theme:vs2012\">public void Update(T data)\r\npublic void Update(T data, bool overwrite)\r\npublic bool TryUpdate(out T data)<\/pre>\n<p>The big challenge facing a developer, is whether to simply overwrite the entire object or just the differences. While working with \u2018changes only\u2019 may prove faster, it also demands careful byte accounting.<\/p>\n<h3>Key Management<\/h3>\n<p><em>MemoryMap<\/em> manages instances with a user provided string in the constructor. While intuitive for demonstration purposes, it lends itself to errors. For example, allowing any value for the what purports to be the same instance almost guarantees different instances between users.<\/p>\n<p>Requirements will likely drive the design of some form of key management. For simple use cases, names based on shared constants as shown might work well enough.<\/p>\n<pre class=\"lang:c# theme:vs2012\">public class KeyNames\r\n{\r\n    public const string Users = \"Users\";\r\n    public const string CurrentCatalog = \"CurrentCatalog\";\r\n    public const string ContactEmails = \"ContactEmails\";\r\n}<\/pre>\n<p>One intriguing possibility might be caching keys within its own <strong>MemoryMap<\/strong> instance. (OK, I digress. Didn\u2019t I say it\u2019s all about requirements?)<\/p>\n<h3>Locking<\/h3>\n<p>The employed locking scheme to share resources is blunt. Opportunities exist for enhancing it, such as applying some form of write-only locking. Developers experienced with multithreaded applications, though, might rightfully get nervous with gratuitous cleverness.<\/p>\n<h3>File Management<\/h3>\n<p>Let\u2019s be honest, saving data to a temp directory on the root drive is not too clever. Expect any production version of <em>MemoryMap<\/em> to explore other, more robust, secure, enterprise-specific options. For example, deleting orphan data files on permanent server instance strikes one as an inevitable feature.<\/p>\n<h3>Serialization<\/h3>\n<p>How <strong>MemoryMap<\/strong> manages serialization constitutes the biggest implementation challenge for any practical caching solution. The sample in this article relied on the somewhat limited, albeit easy to use, .NET <strong>BinaryFormatter<\/strong> for writing and reading data to a file. It is not inconceivable, though challenging, to read and write individual bytes to overcome such limitations. Likewise, maybe the performance benefits of working with binary data are not as important as easily serializing large objects with JSON or some other string-based serialization technology.<\/p>\n<p>Somewhat related to enhancing serialization is object version management. While not generally a concern for most caching solutions, the customized nature of <em>MemoryMap<\/em> lends itself to version checking if the need exists.<\/p>\n<h3>Time to Live (TTL)<\/h3>\n<p>Most caching solutions include some form of expiring cached contents. Implementing such a feature does not ask any significant questions. Just add a date time check and voila. The challenge becomes cleaning up supporting file-based resources as noted above when file management was discussed.<\/p>\n<h2>Conclusion<\/h2>\n<p><a id=\"post-78283-_2et92p0\"><\/a> The sample caching application discussed in this article demonstrated a .NET <em>MemoryMappedFile<\/em> based solution for efficiently sharing objects on the same node. It highlighted the basic mechanics and concerns for building a real-world version. Possibly the biggest challenge facing a developer might be deciding which features to add and when the totality of such additions suggest bypassing performance concerns and employing an existing well-known caching solution.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sharing and reusing large objects between components and services can save time and computing resources. Tom Fischer explains how to take advantage of the Memory Mapped Files feature of .NET to help boost performance.&hellip;<\/p>\n","protected":false},"author":200451,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143538],"tags":[],"coauthors":[17935],"class_list":["post-78283","post","type-post","status-publish","format-standard","hentry","category-dotnet-development"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78283","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/200451"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=78283"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78283\/revisions"}],"predecessor-version":[{"id":78322,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78283\/revisions\/78322"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=78283"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=78283"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=78283"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=78283"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}