Multi-threaded Asynchronous Programming in C#... Event-Driven Architecture. Part IV.


Part I. Overview.

One of the shifts we have to make when switching to a multi-threaded architecture is one of thinking of all our methods in terms of events.  Just like when we had to make the shift in thinking from procedural coding models to thinking in terms of objects, we have to break down our "view" a bit further and divide things up more granularly.  Now we need to think of all methods not as blocks of code by themselves, but in terms of requests and responses and we now need develop from the viewpoint that these messages won't be synchronized.

 We also have to be careful of data corruption with the async model.  If we have two threads on different processors accessing some data such as a member variable at the same time we can be in trouble if either one is performing a write operation because the write can be half-done by one thread while the other starts reading or writing and our data becomes corrupt.  This is manageable by using thread synchronization techniques, the most common (and most performant) of which is the "lock" keyword, which we'll cover in a later article.   Just as a side note though, we want to avoid locking threads as much as possible because it defeats the purpose of multi-threading in the first place because it causes our threads to hang out and wait for access to a block of code and therefore they are not working and the code is not very efficient.  Sometimes we just can't live without them though. The important thing, for now, is just to be weary of the dangers in a multi-threaded environment.

Part II. Async File I/O.

Making async calls against file I/O operations makes a lot of sense because it is often the chips on the storage device we are waiting for to perform the operation in order for us to continue our processing and at a certain point the request is handed off and our CPU has absolutely no responsibilities for the i/o operation. By using async file i/o requests, we can keep our main thread chugging away at other code while we wait for the hardware to do it's job.

To do async file I/O requests, we have to specify that our operation will be an asynchronous one by using the magical flag: FileOptions.Asynchronous.  Without this we won't be processing asynchronously.

In order to get things set up, we need to new up a FileStream object specifying we want a async operation.

FileStream stream = File.Create(fileName, 8 * 1024, FileOptions.Asynchronous);

Next, we create the buffer and send the request to the ThreadPool.  We designate the "EndFileWrite()" method will handle the response and pass some data to manage operations on the response side of things:

Byte[] buffer = Encoding.ASCII.GetBytes(contents);
stream.BeginWrite(buffer, 0, buffer.Length, EndFileWrite, new Object[] { stream, fileName });

For the state object, I crated an object array containing the filename and the stream so we can cleanup everything when the write is done and send off notification that the file has been written.  We can better accomplish the same thing as using the object array by using the anonymous method trick I used in my previous articles on this subject but the demo code is not as clear.  When we use the anonymous method to pass variables into the response code block, the compiler basically does the same thing that I'm doing here by building a custom data transport class so this seems like a clearer sample.  I have both implementations in the source code for this article so you can have a look, but I won't get into the anonymous method pattern here.

For an async read, we'll also be using a FileStream object and use our magic flag again:

FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 8 * 1024, FileOptions.Asynchronous);

The rest is basically the same as the write operation, we specify a buffer and begin reading, passing the method responsible for handling the response ("EndFileRead()") and references to some data we'll be needing to cleanup:

Byte[] buffer = new Byte[stream.Length];

stream.BeginRead(buffer, 0, buffer.Length, EndFileRead, new Object[]{stream, buffer});

Inside the response methods we need to pull out data back out and close/dispose the stream object.  Note that we can't use the "using" block when we declare the file read in the requesting code block because the stream would be disposed before we have received our response.  Here is the code we need to get data back from the file during the response to our read request:

Object[] state = result.AsyncState as Object[];
FileStream stream = state[0] as FileStream;
Byte[] buffer = state[1] as Byte[];
String contents = Encoding.ASCII.GetString(buffer);

#region Cleanup

if (stream != null)
{
    stream.Close();
    stream.Dispose();
}

#endregion

One of the dangers in this architecture is not properly cleaning up after objects, especially those that implement IDisposible because we can go out of scope and loose all references to an object and it can hit the GC without having had the Dispose() method called which is really sloppy.  We just have to be aware of this "gotcha" and be extra vigilant with our house keeping if we are using this approach to make sure we keep things lean-mean-n-clean.

Part III. Wiring Up.

Ok, now for the fun part:  Getting it all wired together.  What we'll build is a small console app that will write some random info to a file and, once written, read it back in and write it to the console.  This is just for demonstration purposes but demonstrates what we generally do with a lot of apps -- read and write data somewhere.  This technique could also be applied to a SqlServer database (if you are interested, check out my earlier brief article on async SqlServer calls). I'll be using a Generic EventArgs<> class for the events which I also explained in an earlier article.  The sample code has the complete solution in addition to demo code for how to anonymous methods to provide a more elegant implementation.  I'll just go over the details here -- if I wrote the source code correctly it should be readable by human as well as machine. 

I'll put all the async calls in a stand-alone static class called "Async".  This class will be responsible to managing the calls and notifying any listeners when the calls are completed.

We will be required to fire off two events: Our first event will be firing when file write is completed and we'll use the event to transport the name of the file written to.  The next event will be firing when the file read is completed and we'll use this event to transport the contents of the file so we Console.Write() them.

private static event EventHandler<EventArgs<string>> m_fileWriteCompleted, m_fileReadCompleted;

We'll have two methods for writing files:

        public static void BeginFileWrite(string fileName, string contents)

        public static void EndFileWrite(IAsyncResult result)

BeginFileWrite() is responsible for initiating the file write and wiring up the response method which is our EndFileWrite();

EndFileWrite() is responsible for stream cleanup and then broadcasting notification that it has completed a job by firing the appropriate event when it is done.

            #region Fire Event Notification

            if (null != m_fileWriteCompleted)
                m_fileWriteCompleted(null, new EventArgs<string>(fileName));

            #endregion

Our main method will be listening for this event to determine when the file-write is done so it can begin reading the file in order to display it.

We'll use the same basic pattern of call-response-raiseEvent on the read side of things.  So basically what we'll have is.

1)      Main sends and async file-write request

2)      BeginFileWrite() will issue the request and designate the response method (EndFileWrite())

3)      EndFileWrite() will receive notification when the write is done, cleanup and fire off the FileWritten event passing the name of the file that was written to.

4)      Main will pick up the FileWritten event and then send an asyc file-read request

5)      BeginFileRead() will issue the read request and designate the response method (EndFileRead())

6)      EndFileRead() will then receive notification when the read is done, cleanup and fire off the FileRead event passing the contents of the read

7)      Main will pick up the FileRead event and display the contents to the console

This can be confusing the first couple times through, especially when debugging because stepping through code asynchronously can make anyone go cross-eyed, but if you get the basic structure and patterns we used, it should be comprehensible after a few passes.

Part IV. Wrap up.

I hope you found this article interesting.  In an upcoming article I'm working on now, I'll cover wiring all this madness to the front end to provide users with responsive win apps, async web forms, or scalable web services.

Until next time,

Happy coding

Up Next
    Ebook Download
    View all
    Learn
    View all