Author: Cole Francis, Architect
BACKGROUND PROBLEM
Many moons ago, I was approached by a client about the possibility of injecting a COM wrapped .NET assembly between two configurable COM objects that communicated with one another. The basic idea was that a hardware peripheral would make a request through one COM object, and then that request would be would intercepted by my new COM object which would then prioritize a hardware object’s data in a cross-process, global singleton. From there, any request initiated by a peripheral would then be reordered using the values persisted in my object.
Unfortunately, the solution became infinitely more complex when I learned that peripheral requests could originate from software running on different processes on the same machine. My first attempt involved building an out-of-process storage cache used to update and retrieve data as needed. Although it all seemed perfectly logical, it lacked the split-second processing speed that the client was looking for. So, next I tried to reading and writing data to shared files on the local file system. This also worked but lacked split-second processing capabilities. As a result, I ended up going back and forth to the drawing board before finally implementing a global singleton COM object that met client’s needs (Yes, I know it’s an anti-pattern…but it worked!).
Needless to say, the outcome was a rather bulky solution, as the intermediate layer of software I wrote had to play nicely with COM objects that it was never intended to live between, as well as adhere to specific IDispatch interfaces that weren’t very well documented, and it reacted to functionality that at times seemed random. Although the effort was considered highly successful, development was also very tedious and came at a price…namely my sanity. Looking back on everything well over a decade later and applying the knowledge that I possess today, I definitely would have implemented a much more elegant solution using an API stack that I’ll go over in just a minute.
As for now, let’s switch gears and discuss a something that probably seems completely unrelated to the topic at hand, and that is memory functions. Yes, that’s right…I said memory functions. It’s my belief that when most developers think of storing object and data in memory, two memory functions immediately come to their mind, namely the Heap and Virtual memory (explained below). While these are great mechanisms for managing objects and data internal to a single process, neither of the aforementioned memory-based storage facilities can be leveraged across multiple processes without employing some sort of out-of-process mechanism to persist and share the data.
1) Heap Memory Functions: Represent instances of a class or an array. This type of memory isn’t immediately returned when a method completes its scope of execution. Instead, Heap memory is reclaimed whenever the .NET garbage collector decides that the object is no longer needed.
2) Virtual Memory Functions: Represent value types, also known as primitives, which reside in the Stack. Any memory allocated to virtual memory will be immediately returned whenever the method’s scope of execution completes. Using the Stack is obviously more efficient than using the Heap, but the limited lifetime of value types makes them implausible candidates to share data between different classes…let alone sharing data between different processes.
BRING ON MEMORY MAPPING
While most developers focus predominantly on managing Heap and Virtual memory within their applications, there are also a few other memory options out there that are sometimes go unrecognized, including “Local, Global Memory”, “CRT Memory”, “NT Virtual Memory”, and finally “Memory-Mapped Files”. Due to the nature of our subject matter, this article will concentrate solely on “Memory-Mapped Files” (highlighted in orange in the pictorial below).
To break it down into layman’s terms, a memory-mapped file allows you to reserve an address space and then commit physical storage to that region. In turn, the physical storage stems from a file that is already on the disk instead of the memory manager, which offers two notable advantages:
1) Advantage #1 – Accessing data files on the hard drive without taking the I/O performance hit due to the buffering of the file’s content, making it ideal to use with large data files.
2) Advantage #2 – Memory-mapped files provide the ability to share the same data with multiple processes running on the same machine.
Make no mistake about it, memory-mapped files are the most efficient way for multiple processes running on a single machine to communicate with one another! In fact, they are often used as process loaders in many commonly used operating systems today, including Microsoft Windows. Basically, whenever a process is started the operating system accesses a memory-mapped file in order to execute the application. Anyway, now that you know this little tidbit of information, you should also know that there are two types of memory-mapped files, including:
1) Persisted memory-mapped files: After a process is done working on a piece of data, that mapped file can then be named and persisted to a file on the hard drive where it can be shared between multiple processes. These files are extremely suitable for working with large amounts of data.
2) Non-persisted memory-mapped files: These are files that can be shared between two or more disparate threads operating within a single process. However, they don’t get persisted to the hard drive, which means that their data cannot be accessed by other processes.
I’ve put together a working example that showcases the capabilities of persisted memory-mapped files for demonstration purposes. As a precursor, the example depicts mutually exclusive thoughts conjured up by the left and right halves of the brain. Each thought lives and gets processed using its own thread, which in turn gets routed to the cerebral cortex for thought processing. Inside the cerebral cortex, short-term and long-term thoughts get stored and retrieved in a memory-mapped file that’s availability is managed by a mutex.
A mutex is an object that allows multiple program threads to synchronously share the same resource, such as file access. A mutex can be created with a name to leverage persisted memory-mapped files, or the mutex can be left unnamed to utilize non-persisted memory-mapped files.
In addition to this, I’ve also assembled another application that runs as a completely different process on the same physical machine but is still able to read and write to the persisted memory-mapped file created by the first application. So, let’s get started!
APPLYING VIRTUAL MEMORY TO HUMAN MEMORY
In the code below, I’ve created a console application that references two objects in Heap memory, and they are TheLeftBrain.Stimuli() and TheRightBrain.Stiuli(). I’ve accounted for asynchronous thought processes stemming from the left and right halves of the brain by employing an asynchronous LINQ operation that kicks off two asynchronously blocking sub-threads (i.e. One for the left half of the brain and the other for the right half of the brain). Once the sub-threads are kicked off, the primary thread blocks any further operations until the sub-threads complete their operations and return (or optionally error out). I’ve highlighted the code in orange where I’m performing the asynchronous LINQ operation):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using LeftBrain;
using RightBrain;
namespace LeftBrainRightBrain
{
class Program
{
///
/// The main entry point into the application
///
///
static void Main(string[] args)
{
// Performs an asynchronous operation on both the left and right halves of the brain
try
{
LeftBrain.Stimuli leftBrainStimuli = new LeftBrain.Stimuli();
RightBrain.Stimuli rightBrainStimuli = new RightBrain.Stimuli();
// Invoke a blocking, parallel process
Parallel.Invoke(() =>
{
leftBrainStimuli.Think();
}, () =>
{
rightBrainStimuli.Think();
});
Console.ReadKey();
}
catch (Exception)
{
throw;
}
}
}
}
At this point, each asynchronous sub-thread calls its respective Stimuli() class. It should be obvious that both the LeftBrain() and RightBrain() objects are fundamentally similar in nature and therefore share interfaces and inherit from the same base class object, with the only significant differences being the types of thoughts they invoke and the additional millisecond I added to Sleep() invocation to the RightBrain() class to simply show some variance between the manner in which the threads are able to process.
Nevertheless, each thought lives in its own isolated thread (making them sub-sub threads) that passes information along to the Cerebral Cortex for data committal and retrieval. Here is an example of the LeftBrain() class and its thoughts:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using CerebralCortex;
namespace LeftBrain
{
///
/// Stimulations from the right half of the brain
///
///
public class Stimuli : Memory, IStimuli
{
///
/// Stores memories in a list
///
///
private List memories = new List();
///
/// An overloaded constructor
///
///
public void Think()
{
try
{
string threadName = string.Empty;
int threadCounter = 0;
// Add a list of left brain memories
memories.Add("The area of a circle is π r squared.");
memories.Add("The Law of Inertia is Isaac Newton's first law.");
memories.Add("Richard Feynman was a physicist known for his theories on quantum mechanics.");
memories.Add("y = mx + b is the equation of a Line, standard form and point-slope.");
memories.Add("A hypotenuse is the longest side of a right triangle.");
memories.Add("A chord represents a line segment within a circle that touches 2 points on the circle.");
memories.Add("Max Planck's quantum mechanical theory suggests that each energy element is proportional to its frequency.");
memories.Add("A geometry proof is a written account of the complete thought process that is used to reach a conclusion");
memories.Add("Pythagorean theorem is a relation in Euclidean geometry among the three sides of a right triangle.");
memories.Add("A proof of Descartes' Rule for polynomials of arbitrary degree can be carried out by induction.");
// Recount your memories
memories.ForEach(memory =>
{
this.ProcessThought(string.Format("Thread: {0} (Left Brain)", threadCounter += 1), memory);
});
}
catch (Exception)
{
throw;
}
}
///
/// Controls the thought process for this half of the brain
///
///
public void ProcessThought(string threadName, string memory)
{
try
{
Thread.Sleep(3000);
Thread monitorThread = null;
// Spin up a new thread delegate to invoke the thought process
monitorThread = new Thread(delegate()
{
base.InvokeThoughtProcess(threadName, memory);
});
// Name the thread and start it
monitorThread.Name = threadName;
monitorThread.Start();
}
catch (Exception)
{
throw;
}
}
}
}
Likewise, shown below is an example of the RightBrain() class and its thoughts. Once again, the RightBrain() differs from the LeftBrain() mainly in terms of the types thoughts that get invoked, with the left half of the brain’s thoughts being more cognitive in nature and the right half of the brain’s thoughts being more artistic:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using CerebralCortex;
namespace RightBrain
{
///
/// Stimulations from the right half of the brain
///
///
public class Stimuli : Memory, IStimuli
{
///
/// Stores memories in a list
///
///
private List memories = new List();
///
/// An overloaded constructor
///
///
public void Think()
{
try
{
string threadName = string.Empty;
int threadCounter = 0;
// Add a list of right brain memories
memories.Add("I wonder if there's a Van Gough Documentary on Television?");
memories.Add("Isn't the color blue simply radical.");
memories.Add("Why don't you just drop everything and hitch a ride to California, dude?");
memories.Add("Wouldn't it be cool to be a shark?");
memories.Add("This World really is my oyster. Now, if only I had some cocktail sauce...");
memories.Add("Why did I stop finger painting?");
memories.Add("Does anyone want to go to a BBQ?");
memories.Add("Earth tones are the best.");
memories.Add("Heavy metal bands rock!");
memories.Add("I like really shiny thingys. Oh, Look! A shiny thingy...");
// Recount your memories
memories.ForEach(memory =>
{
this.ProcessThought(string.Format("Thread: {0} (Right Brain)", threadCounter += 1), memory);
});
}
catch (Exception)
{
throw;
}
}
///
/// Controls the thought process for this half of the brain
///
///
public void ProcessThought(string threadName, string memory)
{
try
{
Thread.Sleep(4000);
Thread monitorThread = null;
// Spin up a new thread delegate to invoke the thought process
monitorThread = new Thread(delegate()
{
base.InvokeThoughtProcess(threadName, memory);
});
// Name the thread and start it
monitorThread.Name = threadName;
monitorThread.Start();
}
catch (Exception)
{
throw;
}
}
}
}
Regardless, the thread delegates spawned in the LeftBrain and RightBrain Stimuli() classes are responsible for contributing to short-term memory, as each thread commits its discrete memory item to a growing list of memories via the Thought() object. Each thread is also responsible for negotiating with the local mutex (highlighted below in orange) in order to access the critical sections of the code where thread-safety becomes absolutely imperative (highlighted below in wheat), as the individual threads add their messages to the global memory-mapped file (highlighted below in silver).
After each thread writes its memory to the memory-mapped file in the critical section of the code, it then releases the mutex (highlighted below in green) and allows the next sequential thread to lock the mutex and safely enter into the critical section of the code. This behavior repeats itself until all of the threads have exhausted their discrete units-of-work and safely rejoin the hive in their respective hemispheres of the brain. Once all processing completes, the block is then lifted by the primary thread and normal processing continues.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Xml.Serialization;
namespace CerebralCortex
{
///
/// The common area of the brain that controls thought
///
///
public class Memory
{
///
/// The local and global mutexes
///
///
Mutex localMutex = new Mutex(false, "CCFMutex");
MemoryMappedFile memoryMap = null;
///
/// Shared memory between the left and right halves of the brain
///
///
static List<string> Thoughts = new List<string>();
///
/// Stores a thought in memory
///
///
private bool StoreThought(string threadName, string thought)
{
bool retVal = false;
try
{
Thoughts.Add(string.Concat(threadName, " says: ", thought));
retVal = true;
}
catch (Exception)
{
throw;
}
return retVal;
}
///
/// Retrieves a thought from memory
///
///
private string RetrieveFromShortTermMemory()
{
try
{
// Returns the last stored thought (simulates short-term memory)
return Thoughts.Last();
}
catch (Exception)
{
throw;
}
}
///
/// Invokes the thought process (uses a local mutex to control thread access inside the same process)
///
///
public bool InvokeThoughtProcess(string threadName, string thought)
{
try
{
// *** CRITICAL SECTION REQUIRING THREAD-SAFE OPERATIONS ***
{
// Causes the thread to wait until the previous thread releases
localMutex.WaitOne();
// Store the thought
StoreThought(threadName, thought);
// Create or open the cross-process capable memory map and write data to it
memoryMap = MemoryMappedFile.CreateOrOpen("CCFMemoryMap", 2000);
byte[] Buffer = ASCIIEncoding.ASCII.GetBytes(string.Join("|", Thoughts));
MemoryMappedViewAccessor accessor = memoryMap.CreateViewAccessor();
accessor.Write(54, (ushort)Buffer.Length);
accessor.WriteArray(54 + 2, Buffer, 0, Buffer.Length);
// Conjures the thought back up
Console.WriteLine(RetrieveFromShortTermMemory());
}
return true;
}
catch (Exception)
{
throw;
}
finally
{
// Releases the lock on the critical section of the code
localMutex.ReleaseMutex();
}
return false;
}
}
}
With the major portions of the code complete, I am now able to run the application and watch the threads add their memories to the list of memories in the memory-mapped file via the critical section of the cerebral cortex code (click on the pictorial below to view the results)…
So, to quickly wrap this article up, my final step is to create a separate console application that will run as a completely separate process on the same physical machine in order to demonstrate the cross-process capabilities of a memory-mapped file. In this case, I’ve appropriately named my console application “OmniscientProcess”.
This application will make a call to the RetrieveLongTermMemory() method in its same class in order to negotiate with the global mutex. Provided the negotiation process goes well, the “OmniscientProcess” will attempt to retrieve the data being preserved within the memory-mapped file that was created by our previous application. In theory, this example is equivalent to having some external entity (i.e. someone or something) tapping into your own personal thoughts.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using CerebralCortex;
namespace OmniscientProcess
{
class Program
{
static Mutex globalMutex = new Mutex(false, "CCFMutex");
static MemoryMappedFile memoryMap = null;
static void Main(string[] args)
{
// Reference the memory object and retrieve our memory-mapped data
CerebralCortex.Memory cerebralMemory = new Memory();
List<string> longTermMemories = cerebralMemory.RetrieveLongTermMemory();
longTermMemories.ForEach(memory =>
{
Console.WriteLine(memory);
});
Console.WriteLine(string.Empty);
Console.WriteLine("Press any key to end...");
Console.ReadKey();
}
///
/// Retrieves all thoughts from memory (uses a global mutex to control thread access from different processes)
///
///
private static List<string> RetrieveLongTermMemory()
{
try
{
// Causes the thread to wait until the previous thread releases
globalMutex.WaitOne();
string delimitedString = string.Empty;
memoryMap = MemoryMappedFile.OpenExisting("CCFMemoryMap", MemoryMappedFileRights.FullControl);
MemoryMappedViewAccessor accessor = memoryMap.CreateViewAccessor();
ushort Size = accessor.ReadUInt16(54);
byte[] Buffer = new byte[Size];
accessor.ReadArray(54 + 2, Buffer, 0, Buffer.Length);
string delimitedThoughts = ASCIIEncoding.ASCII.GetString(Buffer);
return delimitedThoughts.Split('|').ToList();
}
catch (Exception)
{
throw;
}
finally
{
// Releases the lock on the critical section of the code
globalMutex.ReleaseMutex();
}
}
}
}
The aforementioned application has the ability to retrieve the state of the memory-mapped file from an external process at any point in time, except of course when the mutex is locked. It’s the responsibility of the mutex to exercise thread safety, regardless of the originating process, whenever a thread attempts to access the shared address space that comprises the memory-mapped file (see below):
Output 1 – Here’s a partial listing that was retrieved early in the process:
Output 2 – Here’s the full listing that was retrieved after all of the threads committed their data:
Finally, while memory-mapped files certainly aren’t a new concept (they’ve actually been around for decades), they are sometimes difficult to wrap your head around when there’s a sizable number of processes and threads flying around in the code. And, while my examples aren’t necessarily basic ones, hopefully they employ some rudimentary concepts that everyone is able to quickly and easily understand.
To recount my steps, I demonstrated calls to disparate objects getting kicked off asynchronously, which in turn conjure up a respectable number of threads per object. Each individual thread, operating in each asynchronously executing object, goes to work by negotiating with a common mutex in an attempt to commit its respective data values to the cross-process, memory-mapped file that’s accessible to applications running as entirely different processes on the same physical machine.
Thanks for reading and keep on coding! 🙂