Crash course in async and await


I'm going to assume that you know how the async and await keywords work. If you need a refresher, you can read Eric Lippert's extensive exposition of the subject. Here's the short version. People who know how async and await work can go take a nap.

When you write a function that is marked async, then the function is broken up into a series of mini-functions at each await call. The code executes synchronously up until the first await, at which point the rest of the code is scheduled for resumption when the awaited thing produces a result. Optionally, a task is returned so that the caller can schedule its own continuation when the async function executes its return statement.

For example, let's take this function:

async Task<int> GetTotalAsync()
{
  int total = GetStartValue();
  int increment = await GetIncrementAsync();
  return total + increment;
}

This is shorthand for the following, with error checking has been elided for expository simplicity.

Task<int> GetTotalAsync()
{
  int total = GetStartValue();
  return GetIncrementAsync().ContinueWith((incrementTask) => {
    int increment = incrementTask.Result;
    return total + increment;
  });
}

(Actually, that's not really what happens; here are the gory details.)

The point is that the function executes normally until it encounters the first await, at which point it schedules itself as a continuation of the thing being awaited, and returns a new task that represents the continuation. When the thing being awaited completes, execution resumes with the continuation. That continuation might do some work, and then perform another await, which once again schedules itself as a continuation of the thing being awaited. Eventually, the original function runs to completion, at which point the chain of tasks terminates with a result, namely the thing that the original function returned.

Note that when dealing with async functions, you have to distinguish with what the function returns and what it produces as a result when it completes. The return value is the thing that is returned synchronously by the function, typically a task of some sort. When execution reaches the end of the task chain, the task is said to have completed. The thing that comes out the end is called the result.

In other words, there are two ways to call an async function.

var task = SomethingAsync();
var result = await SomethingAsync();

If you call it without await then you get the raw task back. If you call it with await, then when the task completes, you get the result.

People who know how async and await work can start waking up now. You still know the stuff coming up next, but at least you'll be primed for the discussion to come after.

There are three ways of writing an async function:

  • async Task<T> SomethingAsync() { ... return t; }
  • async Task SomethingAsync() { ... }
  • async void SomethingAsync() { ... }

In all the cases, the function is transformed into a chain of tasks. The difference is what the function returns.

In the first case, the function returns a task that eventually produces the t.

In the second case, the function returns a task which has no product, but you can still await on it to know when it has run to completion.

The third case is the nasty one. The third case is like the second case, except that you don't even get the task back. You have no way of knowing when the function's task has completed.

The async void case is a "fire and forget": You start the task chain, but you don't care about when it's finished. When the function returns, all you know is that everything up to the first await has executed. Everything after the first await will run at some unspecified point in the future that you have no access to.

Now that I've set up the story, we'll dig into the consequences next time.

Comments (13)
  1. Joshua says:

    There is no point in calling ReadAsync on a GZipStream (or any arbitrary stream you don’t know what it does). The default is to fulfill it synchronously anyway. FileStream, SocketStream, and NamedPipeStream actually implement ReadAsync but almost nothing else does.

    I’m pretty sure that if you actually call the “void SomethingAsync” without the await keyword it’s really returning a Task, but there’s no good manual on how to resolve Tasks.

    1. RaceProUK says:

      Typically, you’d consume a Task by awaiting, but when you need to do so explicitly, the best I’ve found is .GetAwaiter().GetResult().

      1. Joshua says:

        And if you have more than one task?

        1. Joshua Schaeffer says:

          If you have more than one task, you save them to a collection then pass the collection to Task.WhenAll() or Task.WhenAny(). Then you read back from the task in question.

          Task.Result has the result and a read on it will synchronously block until a result is obtained. You have to make double sure that any continuations the task needs to complete aren’t posted back to the thread you’re blocking on or you will deadlock. Async/await has a few of these common situations which really deserve a book. GetAwaiter() and GetResult() are for the compiler to call, not you (rejoice).

          1. RaceProUK says:

            Task.Result also has a rather annoying habit of wrapping exceptions in an AggregateException, but with GetAwaiter().GetResult(), you get the naked exception.

          2. Joshua says:

            I wouldn’t worry too much about the continuations being posted to my thread. There’s no continuation handler above me.

    2. Jonathan Gilbert says:

      > I’m pretty sure that if you actually call the “void SomethingAsync” without the await keyword it’s really returning a Task, but there’s no good manual on how to resolve Tasks.

      Not the case. The method really has `void` as a return type. The reason this feature exists is to allow an `async` method to operate as an event handler, where the event handler delegate type returns `void`. From what I’ve read, there really is no other good reason to use `async void`.

  2. RaceProUK says:

    I may be wrong, but I believe the reason for the `async void` syntax is compatibility with event handlers, which typically must return `void` (as much as you can return `void`). If it wasn’t for that, the whole nasty case would simply not exist.

    1. Joshua Schaeffer says:

      In practice async void is for anything that accepts a delegate, especially work items to thread pools if you’re scaling hard and tight.

      1. AndyCadley says:

        There’s a Microsoft sample of how to write an Interleave combinatory for awaiting on large numbers of Tasks without suffering the penalties of excessive continuations being creating (if you use WhenAny and do it the naïve way it’s O(n^2)) and that avoids the problems of async void.

  3. GL says:

    Next time I guess you’re talking about async void, in which when an exception escapes the function body as it normally reads, uncaught, after the first await, crash manifests itself, because there’s no exception handler.

  4. cheong00 says:

    Talking about async calls in C#.

    There’s one common mistake on writing async method with anonymous method: you write a try…catch… block that wraps it and forgot to write another to wrap within the anonymous method. It has been asked in a few times in the C# board in MSDN forum why the external try…catch… block doesn’t fire.

    Hope there is static analysis rule to check that. (I didn’t aware of such rule in VS2015)

  5. David Totzke says:

    Stephen Toub of Microsoft has an excellent document on the “Task-based Asynchronous Pattern”.
    https://www.microsoft.com/en-us/download/details.aspx?id=19957

Comments are closed.

Skip to main content