Eduasync part 4: Filling in AsyncTaskMethodBuilder

In part 2, I introduced AsyncVoidMethodBuilder, AsyncTaskMethodBuilder and AsyncTaskMethodBuilder<T>. I showed all the signatures, but the implementations were basically trivial and non-functional. Today, I’m going to change all of that… at least a bit.

In particular, by the end of this post we’ll be able to make the following code actually print out 10 as we’d expect:

private static void Main(string[] args)
{
    Task<int> task = Return10Async();
    Console.WriteLine(task.Result);
}

private static async Task<int> Return10Async()
{
    return 10;
}

Note that there are no await statements – remember that AsyncTaskMethodBuilder<T> is about the boundary between the caller and the async method. We won’t be actually doing anything genuinely asynchronous today – just filling in a piece of the infrastructure.

I’m not going to implement AsyncVoidMethodBuilder or AsyncTaskMethodBuilder, because once you understand how AsyncTaskMethodBuilder<T> works, the others are basically trivial to implement. (Certainly the non-generic AsyncTaskMethodBuilder is too similar to warrant going into; I may potentially revisit AsyncVoidMethodBuilder to investigate its behaviour on exceptions.)

Just as a reminder, this is what we need our AsyncTaskMethodBuilder<T> struct to look like in terms of its interface:

public struct AsyncTaskMethodBuilder<T>
{
    public static AsyncTaskMethodBuilder<T> Create();
    public void SetException(Exception e);
    public void SetResult(T result);
    public Task<T> Task { get; }
}

Frankly, this would be a bit of a pain to implement ourselves… especially if we wanted to do it in a neat way which didn’t introduce a new thread unnecessarily. Fortunately, Parallel Extensions makes it pretty easy.

TaskCompletionSource to the rescue!

The TaskCompletionSource<TResult> type in the framework makes it almost trivial to implement AsyncTaskMethodBuilder<T>. It provides us everything we need – a task, and the ability to specify how it completes, either with an exception or a value. Our initial implementation is really just going to wrap TaskCompletionSource. I suspect the real CTP does slightly more work, but you can get a remarkably long way with simple wrapping:

public struct AsyncTaskMethodBuilder<T>
{
    private readonly TaskCompletionSource<T> source;

    private AsyncTaskMethodBuilder(TaskCompletionSource<T> source)
    {
        this.source = source;
    }

    public static AsyncTaskMethodBuilder<T> Create()
    {
        return new AsyncTaskMethodBuilder<T>(new TaskCompletionSource<T>());
    }

    public void SetException(Exception e)
    {
        source.SetException(e);
    }

    public void SetResult(T result)
    {
        source.SetResult(result);
    }

    public Task<T> Task { get { return source.Task; } }
}

We’ll see how far that takes us – we may need to tweak it a bit, probably around the exception handling. For the moment though, it certainly does everything we need it to. The program runs, and prints out 10 as we’d expect.

This part of the code is now ready for asynchrony – even though nothing we’ve done so far actually creates any different threads. If we call SetResult from one thread while another thread is waiting on the task’s result, the waiting thread will be unblocked as we’d expect.

Conclusion

In some ways I hope you’re disappointed by this, in the same way that looking at the LINQ to Objects implementations can be slightly disappointing. It feels like so much is being given to us for free – the framework already had all this power, so what’s C# 5 really adding to the mix. Well, we’re nearly at the point where I can show you the details of what the compiler’s doing under the hood. That’s where the real power lies, and is why I’m so excited by the feature.

First though, we need to implement the awaiter pattern for Task<T>. That’s the task of our next post, and then we can complete the picture by decompiling the generated code for some programs using genuine asynchrony.

7 thoughts on “Eduasync part 4: Filling in AsyncTaskMethodBuilder”

  1. First of all, congratulations for your excellent work. I don’t understand how the actual task to execute is feeded into the Task property of the TaskCompletionSource class.

    Like

  2. Alberto: The task returned from the TaskCompletionSource represents the overall task of the async method. It’s a *new* Task, not an existing one. We haven’t awaited anything – that will come next. Hope that makes sense – I may update the post to explain this a bit more, but not tonight :)

    Like

  3. alberto, a Task represents two things:
    1. The job that has to be done which completes somewhere in the future; downloading data, calculating the 1337th prime, etc.
    2. The result of that job, also called a future.

    You don’t necessarily need to have a job to create a future. For example you could have this contrived method:

    Future GetFirstChoice() {
    Future fut = new Future();
    OnClick += () => fut.SetResult(Choice); // fut.Result will only be set on the first call, all others will be ignored
    return fut;
    }

    Using a job to to implement GetFirstChoice would include a really big shoehorn.

    What TaskCompletionSource does is give you an easy way to create a Task that represents just a future. So you can implement Task GetFirstChoice() without touching that shoehorn.

    Task GetFirstChoice() {
    var source = new TaskCompletionSource();
    OnClick += () => source.SetResult(Choice); // source.Task.Result will only be set on the first call, all others will be ignored
    return source.Task;
    }

    Like

  4. I have a pointless, tangential question about the code example. :)

    Any particular reason you pass the new instance of TaskCompletionSource to the AsyncTaskMethodBuilder constructor, rather than just allocating it within the constructor itself?

    The constructor parameter seems superfluous, but I’m wondering if there’s an alternative use case to be added to the AsyncTaskMethodBuilder class where it needs that parameter instead of just creating the instance within.

    Like

  5. @pete.d: You mean allocating it in a parameterless constructor? Remember that this is a struct… we don’t get to write our own parameterless constructors…

    Like

Leave a reply to skeet Cancel reply