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

A word of caution on argument validation in Async methods

I have been noticing around in some blogposts and code snippets a pattern being used when validating arguments in Task-returning async methods and that is returning the ArgumentException as part of the Task
instead of immediately failing (directly raising the exception). Something didn’t feel right so i thought i’d try it
out.

Let’s start with defining these 2 async method variations for argument validation :

The first one assigns the exception to the returned Task and the second one immediately raises it the good old-fashioned way.

Next we write our tests that cover the usage scenarios of our async methods :

Both tests try to call our async methods with a null argument by both awaiting and not awaiting them (fire and forget).
And here is what we get after running it :

As expected, we notice that the thrown ,argument validating, exception when assigned to the returning Task and when not awaited by the caller is completely lost! Like it never happened. The Task “swallowed” it.

In most cases this is the expected behavior, when the exception is part of your Task logic and isn’t awaited, but these kind of exceptions should not go unnoticed! They represent usage errors and should be communicated to the caller fast and always. These can be your usual pre-conditions like argument validation, state validation etc.

One of the lessons from this is that you should also be aware of the implications of your async method not being awaited.

I highly recommend reading the “Task-Based Asynchronous Pattern” document from Microsoft here where
it states :

Exceptions

An asynchronous method should only directly raise an exception to be thrown out of the MethodNameAsync call in response to a usage error*. For all other errors, exceptions occurring during the execution of an asynchronous method should be assigned to the returned Task. This is the case even if the asynchronous method happens to complete synchronously before the Task is returned. Typically, a Task will contain at most one exception.  However, for cases where a Task is used to represent multiple operations (e.g. Task.WhenAll), multiple exceptions may be associated with a single Task.

(*Per .NET design guidelines, a usage error is something that can be avoided by changing the code that calls the method. For example, if an error state results when a null is passed as one of the method’s arguments, an error condition usually represented by an ArgumentNullException, the calling code can be modified by the developer to ensure that null is never passed. In other words, the developer can and should ensure that usage errors never occur in production code.)