Explained: .NET Async and Await

“Explained” is a series in which I endeavor to explain concepts that I’ve often found to trip up developers or teams. In the spirit of the “protégé effect“, I hope to strengthen my own understanding by helping others.

Async and await are keywords, introduced in the .NET Framework some years ago, intended to simplify the development of asynchronous methods. One of the primary goals is to enable developers to craft asynchronous logic without forsaking readability.  Previously, such methods might have been beleaguered with callbacks and continuances. Async and await seek to bring a serial clarity to non-serial logic.

Forget the keywords

The objective is a noble one — readability is crucial to maintainability — but I’ve found that Microsoft’s particular choice of terminology here has created a stumbling block for developers that are new to the async/await paradigm.

Consider this question: what is an asynchronous method?

I think most developers would (correctly) suggest that an asynchronous method is simply one that does not block its caller in the course of executing its own logic. Unfortunately, Microsoft has polluted the lexicon a bit by introducing the ‘async’ method modifier. I find that this leads to ambiguity in discussions and, as a result, hampers a developer’s ability to grasp the concept.

  • Is an asynchronous method one that returns a Task?
  • It is one that’s marked with the async keyword?
  • Is it one that’s named with the conventional Async suffix?

So, the first thing I tell a developer is, “forget the keywords”.

An asynchronous method is exactly what it’s always been: a method that does not block its caller in the course of executing its own logic.

Please continue

As a consumer of an asynchronous method, one typically has the need to take action upon its completion. This can be tricky to coordinate, So, frameworks often provide a means of hooking that completion event through a continuance or callback.

.NET’s native approach to this technique is found in its implementation of Task and, more importantly, the GetAwaiter pattern.

In .NET-land, the implementor of an asynchronous method may choose to return a Task. That Task is our application’s reference to asynchronous activity. Task implements the GetAwaiter pattern, giving us the ability to continue our activities upon its completion. The “awaiter”, among other things, will flag the completion of the asynchronous activity and accept a callback to be executed upon completion.

In other words, it provides us with a means of continuance:

var databaseTask = CallDatabaseAsync();
var taskAwaiter = databaseTask.GetAwaiter();
taskAwaiter.OnCompleted(() => CarryOn());

Async-ing feeling

Now, you’re unlikely to find a snippet exactly like the one above in any real code base. It’s exaggerated for the purpose of conversation but the general technique is a common one. (Imagine the callback hell that you’d find yourself in if you were to implement a chain of such calls.) Thankfully, .NET provides some syntactic sugar to help the medicine go down.

The async and await keywords work in tandem to streamline the implementation of asynchronous methods.

When async is applied to applied to a method signature, it signals to the compiler our intent to use the await keyword within its implementation. A method marked as async must:

  • Return void, Task, or Task<T>
  • Include at least one occurrence of await

Generally speaking, async signals that we’d like the compiler to take care of the Task handling for us.

public async Task<Account> GetAccountAsync(string username)
{
    AccountDto accountDto = await GetAccountFromDbAsync(username);

    return new Account
    {
        FullName = accountDto.FullName,
        Email = accountDto.Email,
        Username = accountDto.Username
    };
}

The result of our method — our return value or an exception that may be thrown — will be lifted into a Task without any additional work on our part. Notice, in the sample above, that I return an Account despite the return type of the method indicating that the return type is Task<Account>. The compiler will take care of lifting the reference that I return into a Task instead.

For methods with nothing to return, the signature of our async method should indicate a return type of Task rather than Task<TResult>. This provides callers with the ability to await completion of the method even though it provides no response. Again, the compiler takes care of lifting our “void” result into a Task.

In both cases, an exception raised in the course of executing the method will also be lifted into the Task.

(While an async method may explicitly specify a void return type, this is generally something to be avoided. It is essentially an asynchronous method that provides no means of continuance. A fire-and-forget, if you will. So, it’s use cases are more limited than the two options described above.)

Await for it

The await keyword will always accompany async. It marks the point in your method at which to resume when an asynchronous activity has completed.

Consider the sample method in the section above. It invokes an asynchronous method to retrieve an account record from a database and then uses the information within the record to populate an Account business model. In other words, we need to make an asynchronous call but we have work to do upon its completion.

The await keyword precedes our call to the database and indicates that this is the point at which we’d like to resume after the database delivers our record. The implementation of that database call will determine exactly what it means to retrieve data asynchronously but what is important to us, as the consumer, is that this is the point at which our logic will continue.

What can I await, you might ask? Well, you can await anything that’s “awaitable”. Generally, this is any method that returns a Task. (In reality, this is any method that returns something that implements the “GetAwaiter” pattern. More on that in a bit.)

As control is returned to our method, await does us another favor. It unwraps the completed Task, providing us with the raw result.

AccountDto accountDto = await GetAccountFromDbAsync(username);

In the sample above, GetAccountFromDbAsync() would indicate a return type of Task<AccountDto>. Because we are awaiting the call however, we can expect to receive simply an AccountDto rather than the Task.

Finally, with our result in hand, we can move on with the rest of our logic.

The hocus pocus

Await can seem rather magical. How does an asynchronous method execute and wind its way back to where we left off anyway?

Sadly, there is no actual magic involved. Like most mysterious things in software development, it’s pretty unremarkable when you peel back the covers.

The compiler is essentially doing the work that you or I would, if we otherwise had to handle this behavior ourselves. It establishes a continuance to envelope the remaining work. (In essence, the compiler builds and manages the “callback hell” for us.)

I referred to the “GetAwaiter” pattern earlier in this post. By leveraging this pattern, the compiler is able to generate a state machine around our asynchronous call and any subsequent logic, which is able to

  • Determine if the Task has completed
  • Register a continuance to transition its state
  • Retrieve the result of the Task
  • Handle any exceptions  that may occur

This compiler generated code accounts for the behavior discussed earlier, which lifts our method’s result into a Task for upstream consumers.

Other resources

I’ve only just grazed the surface when it comes to async and await. There are a number of great resources out there for anyone looking to dive deeper down the rabbit hole.

 

Photo: Wait !!! by mat_n / CC BY 2.0

 

Comments are closed.

Create a website or blog at WordPress.com

Up ↑