C# async/await - How does the program keep track of where "await" was called?

Viewed 195

When an await is encountered by a running thread, what mechanism is used internally to "bookmark" that that particular method will eventually need to resume when the awaited task is done? Consider the following method:

public async void DoSomething()
{
   await Task.Run(() => SomeLongRunningWork());
   // Code to resume when the task is done...
   Console.WriteLine("Resuming...");
}

When the above await is reached, a thread on the thread pool is used to do the long running work. Furthermore, the calling thread immediately exits the DoSomething() method, and will "come back" when the task is done.

How does the calling thread know how to "come back" at some point? What is happening internally to accomplish this?

1 Answers

When a method contains an await, the compiler creates what is called an Async State Machine. The task being awaited is stored off and everything after the await is marked as the task's continuation.

Through the state machine, an AsyncTaskMethodBuilder<>, and a TaskAwaiter<>, the infrastructure keeps track of the execution and synchronization contexts, captures local variables and ultimately tracks the state of the original Task while it waits for completion. It does this by looping (calling an internal MoveNext method) until the task has completed, been cancelled or faulted (due to an exception). Assuming the former, the continuation is then invoked on the appropriate context. When an exception is thrown, the AggregateException you would normally get with a Task is "unwrapped" (throwing the first exception) to mimic the experience of a normal, synchronous method.

This is an overall simplification. There are various optimizations and a bunch of machinery that makes this all work. For a good blog on the topic, I suggest this Microsoft series on async methods.

Related