Understanding Asynchronous Programming with .NET Reflector

24 September 2012
by Nick Harrison

When you are working with the .NET framework, it is great to be able to view, and step into, assemblies. The documentation is handy to have, but nothing beats being able to see and debug the code to understand how it works. Suddenly, the new Async features stop being mystical, and start to make practical sense.

When trying to understand and learn the .NET framework, there is no substitute for being able to see what is going on behind at the scenes inside even the most confusing assemblies, and .NET Reflector makes this possible. Personally, I never fully understood connection pooling until I was able to poke around in key classes in the System.Data assembly. All of a sudden, integrating with third party components was much simpler, even without vendor documentation!

With a team devoted to developing and extending Reflector, Red Gate have made it possible for us to step into and actually debug assemblies such as System.Data as though the source code was part of our solution. This maybe doesn’t sound like much, but it dramatically improves the way you can relate to and understand code that isn’t your own.

Now that Microsoft has officially launched Visual Studio 2012, Reflector is also fully integrated with the new IDE, and supports the most complex language feature currently at our command: Asynchronous processing.

Without understanding what is going on behind the scenes in the .NET Framework, it is difficult to appreciate what asynchronocity actually bring to the table and, without Reflector, we would never know the Arthur C. Clarke Magic that the compiler does on our behalf.

Join me as we explore the new asynchronous processing model, as well as review the often misunderstood and underappreciated yield keyword (you’ll see the connection when we dive into how the CLR handles async).

Trends in the .NET Framework

The designers for the flagship languages seem to be following two trends. One is language parity, and the other centers on preserving the nature of the CLR to the extent that new features are implemented through code generation instead of new opcodes in the IL.

To a certain extent, these two trends affect each other. Language parity obviously seeks to ensure that the two languages are functionally equivalent. They want to try and ensure that any given task can be accomplished in either language with comparable effort. This is a lofty goal, but is somewhat dependent on the preservation of the CLR.

The other trend is really a restriction that they are currently working under. New language features have to be implemented without making any changes to the CLR itself. Instead they are implemented as abstractions in the code generated by the compiler.

We have seen this before on a smaller scale with other recent language enhancements; consider Auto-Implemented properties introduced in C# 3.0. With auto properties, you could implement a class similar to this:

public class FileImport : Entity
{
    public virtual DateTime ImportDate { get; set; }
    public virtual string FileName { get; set; }
    public virtual Servicer Servicer { get; set; }
    public virtual IList<ImportedRecord>
                ImportedRecords { get; set; }
}

Listing 1 – Implementing a CSV parser using auto properties

Looking at this through Reflector under different optimizations, we can see the implementations of the abstractions. Under .NET framework 3.5, this code looks very similar:

CSV parser, with .NET framework 3.5 optimizations

Figure 1 – CSV parser, with .NET framework 3.5 optimizations

But with any pre-3.5 optimization, Reflector ignores the abstractions and we can see the raw code generated by the compiler:

CSV parser with contemporary optimizations removed

Figure 2 – CSV parser with contemporary optimizations removed

Knowing what the auto properties are supposed to do, this compiler-generated code is fairly predictable and is still easily recognizable as based on the original code.

As we will see with yield, async and await, the abstractions implemented by the CLR can be substantially more complex.

Why All the Fuss over Yield?

The yield keyword has been around for a while now, but it is still poorly understood. This humble little keyword brings two key advantages for us; firstly it signals to the constructor that we are dealing with an iterator of some sort. With it, we can write code in a very intuitive loop structure without the overhead of loading the entire list being iterated or built into memory at one time, or having to process or build the entire list at one time.

Secondly, it lowers the memory requirements for your application, as well as allowing you to start processing logic more quickly and even ignore calculating elements that may not be needed.

Consider a simple CSV parser; an obvious implementation may look similar to this:

    public class CsvParserInitial : ParserBase
    {
        public override IEnumerable Parse(
              Stream uploadFile)
        {
            var reader = new StreamReader(uploadFile);
            string line;
            var returnValue = new List();
            while ((line = reader.ReadLine()) != null)
            {
                var data = line.Split(',');
                returnValue.Add(new ImportedRecord(data));
            }
            return returnValue;
        }
    }

Listing 2 – Implementing a CSV parser in an obvious manner

The processing logic is very straightforward, but depending on the size of the upLoadFile, this may have a substantial memory footprint due to the potentially large number of items in the returned list. Additionally, the application may seem excessively unresponsive, since the entire file would have to be parsed before the first record is processed.

public class CsvParserYield : ParserBase
    {
        public override IEnumerable Parse(
           Stream uploadFile)
        {
            var reader = new StreamReader(uploadFile);
            string line;
            
            while ((line = reader.ReadLine()) != null)
            {
                var data = line.Split(',');
                yield return new ImportedRecord(data);
            }
        }
    }

Listing 3 – Implementing a CSV parser using the Yield keyword

Instead of returning a List of imported records, we only return a single record each time this method is called. We also return as soon as the first record is parsed.

Either implementation can be called the same way; it doesn’t matter whether we use CsvParserInitial or CsvParserYield to loop through the results of the parse:

var repository = new ImportRecordRepository();
var data = new CsvParser().Parse(fileStream);
foreach (var record in data)
{
    repository.Save(record);
}

Listing 4 – Calling the CSV parser

However, you will see a dramatic difference once you step through the code with the debugger, or profile the code with a tool like the ANTS Performance Profiler.

We can also see the difference right away by looking behind the scenes with Reflector:

How the CLR implements the initial CSV parser

Figure 3 – How the CLR implements the initial CSV parser, with optimizations

Having decompiled the code, we can see that CsvParserInitial comes through looking pretty much the same as in our code, and CsvParserYield also looks very similar with the optimization set above .NET Framework 2.0:

How the CLR implements the Yield CSV parser

Figure 4 – How the CLR implements the Yield CSV parser, with optimizations

However, when you move the optimization level back to earlier levels of the framework, and remove some of the more contemporary abstractions, we can see what the compiler is doing on our behalf. For starters, the Parse method is unrecognizable:

How the CLR implements the Yield-based Parse method

Figure 5 – How the CLR implements the Yield-based Parse method, without optimizations

It uses a Compiler generated class <Parse>d_0, and looking through this class may be initially confusing…

The Compiler-generated class for the Yield CSV parser

Figure 6 – The compiler-generated class for the Yield CSV parser

…Because one final piece of information is needed before we can dive in to understand what the compiler is actually doing. The key is that, since our data variable is actually an Enumerator, the simple foreach loop that we saw earlier will really be implemented like this:

var iterator = data.GetEnumerator();
while (iterator.MoveNext() )
{
    var item = iterator.Current;
    repository.Save(item);
}

Listing 5 – Implementation of our foreach loop

The GetEnumerator will ensure that we start with a state of 0 and that all of the associated properties are properly initialized:

The code under the hood of GetEnumerator

Figure 7 – The code under the hood of GetEnumerator

Most of the magic actually happens in the MoveNext method:

Under the hood of the MoveNext method in our Yield-based parser

Figure 8 – Under the hood of the MoveNext method in our Yield-based parser

Without any optimizations, this is very intimidating and a great example of why you should never use a goto statement. The compiler created a finite state machine to represent the various states that are possible while enumerating through the file to be parsed. Remember this detail for later!

Fortunately for us mere mortals, we don’t need to delve down to this level - only the brave souls who design compilers have to deal with this level of complexity. Well, them and the folks on the Reflector team who also delve down to this level, unwinding what the compiler did so that we don’t have to.

The bottom line is that we get to write nice easy-to-follow code that can take advantage of some rather sophisticated optimizations, simply by using the yield keyword. Having seen what the compiler does for us, it should be obvious that all of these optimizations go out the window if we blindly slap a ToList() onto the resulting enumeration.

I should also point out than an alternative optimization skips the yield altogether and uses lambda expressions to do everything in a single method, but still manages to keep the parsing process separate from dealing with the parsed objects.

public void ParseAndTakeAction (Stream uploadFile,
                                Action<ImportedRecord> action  )
{
    var reader = new StreamReader(uploadFile);
    string line;

    while ((line = reader.ReadLine()) != null)
    {
        var data = line.Split(',');
        action.Invoke(new ImportedRecord(data));
    }
}

public void ParseUploadedFile ( Stream fileStream)
{
    var parser = new CsvParser();
    var repository = new ImportRecordRepository();
    parser.ParseAndTakeAction(fileStream, 
            p => repository.SaveOrUpdate(p));

}

Listing 6 – Writing a CSV parser using lambda expressions instead of the yield keyword

Awaiting to Async

Few things can be more frustrating from a user’s perspective than an unresponsive application. Regardless of what the application is doing, a user will not tolerate sending an event only to have that event be apparently ignored while the application finishes with something else. Unfortunately, it is common for the UI to “freeze” while waiting on another process to complete, meaning that the UI will not be redrawn, will not respond to move events, and will ignore anything you do while it is waiting.

While this is annoying in a WinForm application, it is even less acceptable in the world of mobile devices with added events like multi touch, rotation, etc.

This is one of many problems solved with the new async and await keywords, which identify (or should be used in) methods that can potentially be a long running process. These methods become suspension points where control can be returned to more active threads while your long running process is finishing things up. If control were not released, the entire program would ordinarily be blocked, but by releasing control back to areas that can use it, the program can continue making progress, making the application appear more responsive.

What Does Async Do?

Instead of trying to explain it, it may be easier to grasp by simply seeing it in action. For this demonstration, let’s consider the implementation of Conway’s Game of Life that is included as a sample in the CTP.

I have always been fascinated by this zero player game developed by the British Mathematician John Horton Conway. It has many esoteric implications in theoretical computer science, but a full survey of the interesting properties of Life is beyond the scope of this article. Wikipedia has a great write-up on some of these interesting details.

We will look at an elegant implementation for displaying the output from this famous algorithm, and see how async comes into play. The heart of this implementation can be seen here:

private async void MainForm_Load(object sender, EventArgs e)
{
    pbLifeDisplay.Image = null;
    int width = pbLifeDisplay.Width, height = pbLifeDisplay.Height;
    var pool = new ObjectPool<Bitmap>(() => new Bitmap(width, height));
    var game = new GameBoard(width, height, .7, pool);
    while (true)
    {
        var bmp = await TaskEx.Run(() => game.MoveNext());
        var old = (Bitmap)pbLifeDisplay.Image;
        pbLifeDisplay.Image = bmp;
        if (old != null) pool.PutObject(old);
    }
}

Listing 7 – Implementing Conways Game of Life using the async and await keywords

Note that TaskEx is from the CTP – Task in 4.5 has this built in. The code sample shown above is for version 4.0 of the framework and the CTP. If you have installed 4.5, simply change the reference from TaskEx to Task.

This is the main loop that drives the process of running through and displaying the various “generations” in the game. The key details for the implementation of the game are in the MoveNext method. If you are interested in the broader details of this implementation, you can examine the implementation of the MoveNext method, but for this article, we are only interested in the MainForm_Load.

Because of the async keyword, other events (such as paint) will continue to run despite the infinite loop. This simple code produces results in a window like the one seen below, which will continually loop through the generations in the game. The form is displayed, can be dragged around, will respond to resize events, and remain responsive despite the infinite loop in the form load event handler.

Conway's Game of Life in action

Figure 9 – Conway's Game of Life in action

Take away the async and the await keywords, and the result is no UI at all because the Load event never completes, and is being run synchronously. No other events will fire.

Those two little keywords transform code that compiles but does not work into code that compiles and works just fine. Let’s take a peek under the hood and see what the compiler does with these keywords.

Under The Hood of Async and Await

Let’s get our hands dirty. Optimized for 4.5, the code looks, if not exactly alike our implementation is at least similar:

Compiler-generated code for our async implementation, with optimizations

Figure 10 – Compiler-generated code for our async implementation, with optimizations

Dialing the optimization back to 3.5, we can see a better perspective of what the compiler has done to work this magic:

Compiler-generated code for our async implementation without optimizations

Figure 11 – Compiler-generated code for our async implementation without optimizations

The compiler has created a new class called <MainForm_Load>d_5, and the MainForm_Load now simply creates an instance of this class and starts it.

Looking at this generated class, we see that it is a state machine which, if you recall, looks similar to the tricks used to make the yield statement work, only even more complex and sophisticated.

The async state machine

Figure 12 – The async state machine

Once again, the magic happens in the MoveNext method, but this time it has a little help from the AsyncVoidMethodBuilder object. AsyncVoidMethodBuilder and AsyncTaskMethodBuilder both use the AsyncMethodBuilderCore to drive through the implementations of the state machine in the generated type. The driver for working through the state machine is the Start method:

The Start method driving the async state machine

Figure 13 – The Start method driving the async state machine

In this case, the TStateMachine passed in is the generated type <MainForm_Load>d__5. The Start method creates the environment for invoking the logic exposed in the MoveNext method of the state machine, and then simply calls this generated method.

Once again, MoveNext is an excellent example of why every programmer is told to never use the Goto statement!

The compiler-generated MoveNext, revealing the functions around of the state machine

Figure 14 – The compiler-generated MoveNext, revealing the functions around of the state machine

Going back to the generated MainForm_Load(see below), we see that the state starts off with a value of -1, meaning that we will hit the default from the case in the MoveNext method.

While many of the variable names have been mangled by the compiler, we should still be able to recognize some of the code from our original method. In fact, this code is all our code up to the While() loop.

Compiler-generated MainForm_Load

Figure 15 – Compiler-generated MainForm_Load

And for comparison:

private async void MainForm_Load(object sender, EventArgs e)
{
    pbLifeDisplay.Image = null;
    int width = pbLifeDisplay.Width, height = pbLifeDisplay.Height;
    var pool = new ObjectPool<Bitmap>
          (() => new Bitmap(width, height));
    var game = new GameBoard(width, height, .7, pool);
    while (true)
    {
        var bmp = await TaskEx.Run(() => game.MoveNext());
        var old = (Bitmap)pbLifeDisplay.Image;
        pbLifeDisplay.Image = bmp;
        if (old != null) pool.PutObject(old);
    }
}

Listing 8 – The human-generated MainForm_Load

Looking at the code at Label_081BD below, we see that it redirects to Label_00D2, and for the curious, note that setting the variable flag2 to “true” has no effect because this is the only place this variable is referenced:

Control flowing through the state machine

Control flowing through the state machine

Figure 16 – Control flowing through the state machine

The code in Label_00D2 invokes the method that we are waiting on through the cached anonymous method delegate variable. It also sets the state to 0, meaning that the next time MoveNext is called we will run the code in the Label_0132:

Preparing the state machine for the next iteration

Figure 17 – Preparing the state machine for the next iteration

This code will ensure that the new state will be -1, which will cause the whole process to start over the next time MoveNext is called, and control will then flow on to Label_0150, which may look familiar from our original logic:

        var bmp = await TaskEx.Run(() => game.MoveNext());
        var old = (Bitmap)pbLifeDisplay.Image;
        pbLifeDisplay.Image = bmp;
        if (old != null) pool.PutObject(old);

Listing 9 – the control logic for our implantation of Conways Game of Life, for comparison.

Now we go back to Label00D2, which will run the task again asynchronously, and start the loop over again.

As you can see, there is a lot of confusing, difficult-to-follow code created on our behalf just by including a couple of key words, which we can now investigate and (with a little patience!) understand, thanks to .NET Reflector. Thankfully, when we’re not digging deep to learn, we can write code that looks good and is easy to follow without having to worry too much about all of the hard work that the compiler goes through.

Conclusion

This is an exciting time to be a programmer, and some of the new language features offer intriguing possibilities. While we don’t have to worry about the details of how these new features are implemented most of the time, having a good understanding of what is happening helps ensure that we work with these new features and not against them.

.NET Reflector is a great tool when you are ready to dive into what the compiler is doing for you. I urge you to go and explore, learn the details, and then rejoice that you don’t have to worry about these details every day.

ANTS Performance Profiler Interested in how your async code performs? The Beta release of ANTS Performance Profiler 8 adds a new async profiling mode to help you understand where applications spend their time while doing async work. Try the Beta.


© Simple-Talk.com