Eduasync part 3: the shape of the async method / awaitable boundary

Last time we looked into the boundary between the caller of an async method and the method itself.

This time I’m going to show the same sort of "skeleton API" but for awaitable types. This is at the heart of C# 5’s async feature: within an async method, you can include "await" expressions. You have to await a value – which could be the return value from a method call, or a property, or the value of a variable. The important thing is that the compile-time type of that expression has the right shape.

Definition time

Consider this code snippet from an async method (separated into two statements for clarity):

Task<string> t = new WebClient().DownloadStringTaskAsync(url);
string text = await t;

I wish to define the following terms:

  • The task of the await expression is the expression which comes after "await"; in this case it’s just "t".
  • The awaitable type is the compile-time type of the task. In this case it’s Task<string>.
  • The awaiter type is the compile-time type of the result of calling GetAwaiter() on the task.
  • The result type is the compile-time return type of the awaiter type’s GetResult method. Here it would be string, when using the CTP or any sensible implementation.

(Of these, "task" and "awaiter type" are part of the draft specification. "Awaitable type" and "result type" aren’t.)

Now, the first two are fairly clear from the code above, but the awaiter and result types aren’t. The compiler validates that calling (foo).GetAwaiter() for a task expression "foo" is valid. It doesn’t have to be an instance method – it can be an extension method, and indeed that’s how it’s implemented in the CTP. Unlike the weird things you can do with LINQ query expressions, GetAwaiter() really does have to be a method call, and the expression can’t just be a type name. Unless you’re trying to do weird stuff, this is very unlikely to be an issue for you. It’s only oddballs like me who try to push the envelope.

With the current CTP the awaitable type can’t be "dynamic", but the aim is for C# 5 to support awaiting dynamic values. I imagine this won’t affect the execution flow significantly though.

The awaitable type only has to support the GetAwaiter() method, as we’ve said. The awaiter type has to have the following accessible members (which can’t be extension methods):

  • bool IsCompleted { get; }
  • void OnCompleted(Action continuation)
  • X GetResult() where X defines the result type for the awaiter, and may be void

The overall type of the await expression is the result type – and it has to be appropriate for any surrounding context. For example, the assignment to the "text" variable will only work in our sample if the result type is "string" or a type with an implicit conversion to string.

A simple (non-functional) awaiter

The sample code for this post comes from project 5 in the source solution (SimpleInt32Awaitable); project 4 is similar but with a void result type. Neither project makes any attempt to do any real awaiting:

// Within some other class
static async void SimpleWaitAsync()
{
    SimpleInt32Awaitable awaitable = new SimpleInt32Awaitable();
    int result = await awaitable;
}
 

public struct SimpleInt32Awaitable
{
    public SimpleInt32Awaiter GetAwaiter()
    {
        return new SimpleInt32Awaiter();
    }
}

public struct SimpleInt32Awaiter
{
    public bool IsCompleted { get { return true; } }

    public void OnCompleted(Action continuation)
    {
    }

    public int GetResult()
    {
        return 5;
    }
}

Note that here both the awaitable type and the awaiter type are structs rather than classes; this is not a requirement by any means.

What does it all mean?

I don’t want to leave this post without any clue of what all of this is used for.

Fairly obviously, GetAwaiter() is called to get an awaiter (clever name, huh?) on which some members are called:

  • IsCompleted is called to determine whether the task has already completed. If it has, we can skip over the next step.
  • If the task hasn’t already completed, we want to return very soon rather than waiting – so OnCompleted is calling with a delegate representing the continuation of the method. The awaiter promises to ensure that the continuation is called when (and only when) the task has completed, possibly with an exception.
  • When we know the task has completed (either synchronously or via the continuation), GetResult is called to retrieve the result. Note that even if the method has a void return type, it still has to be called, as the "result" may be that an exception is thrown.

Conclusion

Awaiters are pretty flexible – which means they can be abused in evil, horrible ways, as we’ll see later on.

So far we’ve seen the basic shape of both boundaries, and now we can start actually implementing them with real, useful code.

2 thoughts on “Eduasync part 3: the shape of the async method / awaitable boundary”

  1. ***
    (Of these, “task” and “awaiter type” are part of the draft specification. “Awaiter type” and “result type” aren’t.)
    ***

    I’m guessing you meant: “task” and “awaitable type” in the first sentence?

    Like

Leave a comment