Roots and magic behind C# Async/Await explained simply

Are you using C#’s new async/await feature?
It is cool right?
You learned how to use it but you feel like you are missing a piece of the puzzle?
It feels like magic.
You tried digging a little deeper to learn how it works under the hood,
you looked at the IL that the compiler generates, read a bit on the subject but still don’t understand it completely?
Don’t worry. It’s simple really!
Let’s try to distill it to it’s essence.

What if i told you that you could simulate the same feature even before the await keyword was introduced and all
you need from the language is Iterator blocks and lambdas?

The first step is understanding how Iterator blocks work. They are transformed by the compiler into State machines. Head over for a bit to the link above and let @JonSkeet do his magic explaining the details.

Good. Now lets see what smart people came up with (CCR Iterators as far back as 2005) to solve the callback hell that asynchrony produces.

CCR Iterators

Iterators are a C# 2.0 language feature that is used in a novel way by the Concurrency and Coordination Runtime (CCR). Instead of using delegates to nest asynchronous behavior, also known as callbacks, you can write code in a sequential fashion, yielding to CCR arbiters or other CCR tasks. Multi-step asynchronous logic can then be all written in one iterator method, vastly improving readability of the code. At the same time maintaining the asynchronous behavior, and scaling to millions of pending operations since no OS thread is blocked during a yield.

Here’s the simplest interface we can use to describe what Async means to us:

public interface IAsync
{
    void Run(Action continuation);
}

Note that i don’t deal with cancellation, exceptions or results at this point and that’s on purpose.
How can these two be combined to make asynchronous workflows look sequential?
Well, some brilliant guy at some point stood up and said :

What if we called MoveNext() inside the continuation of every Async call?

And that was it 🙂

Here is the AsyncIterator logic :

    public static class Async
    {
        public static void Run(this IEnumerable<IAsync> asyncIterator)
        {
            var enumerator = asyncIterator.GetEnumerator();

            Action recursiveBody = null;
            recursiveBody = delegate
            {
                if (enumerator.MoveNext())
                    enumerator.Current.Run(recursiveBody);
            };

            recursiveBody();
        }

        public static IAsync Create(Action<Action> asyncAction)
        {
            return new AsyncPrimitive(asyncAction);
        }
    }

    public class AsyncPrimitive : IAsync
    {
        private readonly Action<Action> asyncAction;

        public AsyncPrimitive(Action<Action> asyncAction)
        {
            this.asyncAction = asyncAction;
        }

        public void Run(Action continuation)
        {
            asyncAction(continuation);
        }
    }

And here is what our code looks like.

        static void Main(string[] args)
        {
            MyAsyncWorkflow().Run();
        }

        public static IEnumerable<IAsync> MyAsyncWorkflow()
        {
            for (int i = 1; i < 4; i++)
            {
                Log("Before Async " + i);
                var idx = i;
                yield return Async.Create((moveNext) => { Log("Running Async " + idx); moveNext(); });
            }

            Log("Workflow done!");
        }

Every time MoveNext() is called the code after the current yield return is called up until the next yield return.
This can happen at any point in time and from any thread because our code is decomposed and transformed into a state machine. At every yield return we give back the asynchronous operation that we want to run and the
AsyncIterator logic takes care of wiring everything up for us.

Here is the output when we run it :

AsyncIteratorRun

How does this relate to C#’s await feature? The IL produced by the compiler is built on this idea.
You can replace in your mind IAsync, IAsync.Run and yield return with Task, Task.ContinueWith and await.
Of course there are a lot more details involved with the full blown C# async/await implementation but the essence is the same.

I hoped this helped you demystify a little bit all the new async/await goodness 🙂

That being said here is how professionals do it 😛 : F# Computation Expressions and F# Asynchronous Workflows

Here are some links for the interested reader :

Stephen Toub’s -> Processing Sequences of Asynchronous Operations with Tasks

Tomas Petricek’s -> Asynchronous Programming in C# using Iterators

Jeffrey Richter’s -> Jeffrey Richter and his AsyncEnumerator

Jon Skeet’s -> Eduasync Series

Advertisements