Rise of the IAsyncStateMachines

Whenever you use async/await pair, the compiler performs a lot of work creating a class that handles the coordination of code execution. The created (and instantiated) class implements an interface called IAsyncStateMachine and captures all the needed context to move on with the work. Effectively, any async method using await will generate such an object. You may say that creating objects is cheap, then again I’d say that not creating them at all is even cheaper. Could we skip creation of such an object still providing an asynchronous execution?

The costs of the async state machine

The first, already mentioned, cost is the allocation of the async state machine. If you take into consideration, that for every async call an object is allocated, then it can get heavy.

The second part is the influence on the stack frame. If you use async/await you will find that stack traces are much bigger now. The calls to methods of the async state machine are in there as well.

The demise of the IAsyncStateMachines

Let’s consider a following example:

public async Task A()
{
    await B ();
}

Or even more complex example below:

public async Task A()
{
    if (_hasFlag)
    {
        await B1 ();
    }
    else
    {
        await B2 ();
    }
}

What can you tell about these A methods? They do not use the result of  the Bs. If they do not use it, maybe awaiting could be done on a higher level? Yes it can. Please take a look at the following example:

public Task A()
{
    if (_hasFlag)
    {
        return B1 (); 
    }
    else
    {
        return B2 ();
    }
}

This method is still asynchronous, as asynchronous isn’t about using async await but about returning a Task. Additionally, it does not generate a state machine, which lowers all the costs mentioned above.

Happy asynchronous execution.

10 thoughts on “Rise of the IAsyncStateMachines

  1. The downside of this is that you lose important stack trace information you will no longer know that B was called through A if an exception is thrown from B. That might be acceptable in certain situations and, therefore, worth the perf tradeoff, but everyone should be aware of the consequence.

  2. “If you take into consideration, that for every async call an object is allocated, then it can get heavy.” The async state machines are highly optimized to avoid as much allocation as possible. While it does allocate objects, usually , the underlying I/O operation being done will always be more expensive than the state machines allocation. That is, if you’re using async code on a hot path, you definitely should think if it’s possible to defer the awaiting to the highest stackframe to save allocation.

  3. One important note: do not directly return a Task in this way from within a using block, or the disposables that may be required by the asynchronous task (e.g. dataReader.ReadNextAsync()) will be disposed and weird stuff will ensue,

Comments are closed.