How Async works
async/await keywords
Methods marked with async
may use the await
keyword. The async
keyword was
introduced for compatibility with older code, which could’ve used await
as a
name of variables or other things. This way, if such older code gets upgraded to
.NET > 4.5 (Framework), it will continue to work. Only after the async
keyword
is added, the await
becomes a keyword in a method.
State Machine
The code that uses async/await is just a syntactic sugar. In reality, that code
gets turned into a state machine where state transitions happen whenever await
is encountered. The async
methods get turned into classes representing those
state machines.
Here’s an example stolen from Microsoft DevBlog:
Before:
class StockPrices
{
private Dictionary<string, decimal> _stockPrices;
public async Task<decimal> GetStockPriceForAsync(string companyId)
{
await InitializeMapIfNeededAsync();
_stockPrices.TryGetValue(companyId, out var result);
return result;
}
private async Task InitializeMapIfNeededAsync()
{
if (_stockPrices != null)
return;
await Task.Delay(42);
// Getting the stock prices from the external source and cache in memory.
_stockPrices = new Dictionary<string, decimal> { { "MSFT", 42 } };
}
}
After:
class GetStockPriceForAsync_StateMachine
{
enum State { Start, Step1, }
private readonly StockPrices @this;
private readonly string _companyId;
private readonly TaskCompletionSource<decimal> _tcs;
private Task _initializeMapIfNeededTask;
private State _state = State.Start;
public GetStockPriceForAsync_StateMachine(StockPrices @this, string companyId)
{
this.@this = @this;
_companyId = companyId;
}
public void Start()
{
try
{
if (_state == State.Start)
{
// The code from the start of the method to the first 'await'.
if (string.IsNullOrEmpty(_companyId))
throw new ArgumentNullException();
_initializeMapIfNeededTask = @this.InitializeMapIfNeeded();
// Update state and schedule continuation
_state = State.Step1;
_initializeMapIfNeededTask.ContinueWith(_ => Start());
}
else if (_state == State.Step1)
{
// Need to check the error and the cancel case first
if (_initializeMapIfNeededTask.Status == TaskStatus.Canceled)
_tcs.SetCanceled();
else if (_initializeMapIfNeededTask.Status == TaskStatus.Faulted)
_tcs.SetException(_initializeMapIfNeededTask.Exception.InnerException);
else
{
// The code between first await and the rest of the method
@this._store.TryGetValue(_companyId, out var result);
_tcs.SetResult(result);
}
}
}
catch (Exception e)
{
_tcs.SetException(e);
}
}
public Task<decimal> Task => _tcs.Task;
}
public Task<decimal> GetStockPriceForAsync(string companyId)
{
var stateMachine = new GetStockPriceForAsync_StateMachine(this, companyId);
stateMachine.Start();
return stateMachine.Task;
}
The TaskCompletionSource
is a crucial player in the Task-ecosystem. This is
what controls the state of the task in the background.