Eduasync part 6: using the infrastructure manually

Now that we’ve got the infrastructure for both returning a task from an async method, and awaiting a Task<T>, we’re going to look at what the compiler does for us. I always find that the best way of appreciating a feature like this is to consider what we’d have to do without it. Later on we’re going to see what the C# compiler does in terms of decompiling the actual generated code, but first we’ll look at what we might do to achieve the same end result, using the same infrastructure.

Of course, if we were doing all of this manually we might not choose to use the same infrastructure to start with, but I’m not sure that it would end up being much cleaner anyway.

A simple async method

First, let’s look at the C# 5 async method we want to emulate. It’s pretty simple:

private static async Task<int> ReturnValueAsyncWithAssistance()
{
    Task<int> task = Task<int>.Factory.StartNew(() => 5);

    return await task;
}

Now obviously we could just return the task here to start with – the async method isn’t providing any real benefit. But the simplicity is helpful for showing an equivalent. You can always imagine that there’s more going on between awaiting the result of the task and returning our actual result.

What do we need to do?

Before I include the actual code, let’s consider what we know about the async method execution model:

  • We’ll create an AsyncTaskMethodBuilder to allow us to return a Task<int>.
  • The method will start synchronously, running all the code before the first (and in this case only) await expression in the normal way.
  • The awaitable expression will be evaluated, and GetAwaiter() called to get a suitable awaiter value.
  • We’ll ask the awaiter whether the awaitable has already completed, using the IsCompleted property
    • If it’s already completed, we get the result by calling GetResult(). We can set the result on the builder, and return the builder’s task.
    • Otherwise, create a continuation delegate representing "the rest of the method". We then return the builder’s task.
      • The continuation has to fetch the result by calling GetResult() on the same awaiter, set the result on the builder, and return. (It’s not returning a Task<T> – the continuation is just an Action.)
  • If an exception occurs at any point, whether in the original synchronous call or the continuation, we need to set that exception in the builder, and return (either returning the builder’s task or nothing, depending on the context).

Even describing it makes it sound significantly worse than the async method. Let’s take a look.

Bring on the code!

Here’s the manual method in all its glory:

private static Task<int> ReturnValueAsyncWithoutAssistance()
{
    AsyncTaskMethodBuilder<int> builder = AsyncTaskMethodBuilder<int>.Create();

    try
    {
        Task<int> task = Task<int>.Factory.StartNew(() => 5);

        TaskAwaiter<int> awaiter = task.GetAwaiter();
        if (!awaiter.IsCompleted)
        {
            // Result wasn’t available. Add a continuation, and return the builder.
            awaiter.OnCompleted(() =>
            {
                try
                {
                    builder.SetResult(awaiter.GetResult());
                }
                catch (Exception e)
                {
                    builder.SetException(e);
                }
            });
            return builder.Task;
        }

        // Result was already available: proceed synchronously
        builder.SetResult(awaiter.GetResult());
    }
    catch (Exception e)
    {
        builder.SetException(e);
    }
    return builder.Task;
}

Ugly, isn’t it? Even so, it’s actually somewhere simpler than the real autogenerated code, which we’ll see in the next part. Hopefully if you read the code and comments bearing in mind the bullet points above, it should be reasonably easy to see how it works. However:

  • Currently we’re building a continuation delegate specific to this particular await. Imagine if we had several await expressions. Each would need "the rest of the method" specified as its own delegate, and we’d end up building multiple delegate instances over time.
  • It would be a nightmare to handle loops in this way.
  • We’ve got repeated code – not a lot in this case, but anything we had between the "await" and the "return" would need to be in both flows.
  • You don’t really want to be writing this code by hand anyway, if you can help it.

Having said all of that, it works. Just don’t expect me to write any more complicated async methods this way :)

Conclusion

The steps given here are still going to be valid with the compiler-generated code, but they’ll take a slightly different form. Next time we’ll decompile the generated code for the exact same async method, before we move on to look at more complicated cases.

4 thoughts on “Eduasync part 6: using the infrastructure manually”

  1. This might be a too basic question – I only have simple understanding of threads. I found myself too confused by the position of the commented line:

    if (!awaiter.IsCompleted) {
    // Result wasn’t available. Add a continuation, and return the builder.
    awaiter.OnCompleted(() =>

    We are dealing with asynchronous code. Is it possible awaiter.IsCompleted is changed between the if and the callback binding?

    I’ve rechecked the implementation on part 5, it seems to be calling
    task.ContinueWith(ignored => action(), TaskScheduler.Current);
    , which is http://msdn.microsoft.com/en-us/library/dd321405.aspx

    Is this possible? If so, will the action be executed anyway?
    Again, it’s very possible I’m missing something basic.
    Thanks.

    Like

  2. @Kobi: Yes, it’s possible that IsCompleted will be false when it’s checked, and then become true before we get as far as calling OnCompleted. That’s okay though – when we add the continuation, it will be scheduled immediately (with the current task scheduler).

    Like

  3. Thanks for the quick response!
    I figured that was the case, but couldn’t find it documented.
    If this is the case, can’t you remove the condition and rely on OnCompleted in both cases (for completed and non-completed tasks)?
    Is if(!awaiter.IsCompleted) an optimization?
    It looks like it can eliminate the code duplication, but it may also have a huge overhead I’m not aware of.
    Thanks.

    Like

  4. @Kobi: Yes, only attaching the continuation if you need to is an optimization, but a potentially important one. It means that if everything *is* already completed (imagine a Lazy type implementation, where *usually* it may be already available) using an async method has very little overhead indeed.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s