await anything;

await anything;

Rate This
  • Comments 14

One of the very cool things about the new await keyword in C# and Visual Basic is that it’s pattern based.  It works great with Task and Task<TResult>, and awaiting those two types will represent the vast majority of uses, but they’re by no means the only types that can be awaited.  The languages support awaiting any instance that exposes the right method (either instance method or extension method): GetAwaiter.  A GetAwaiter needs to implement the INotifyCompletion interface (and optionally the ICriticalNotifyCompletion interface) and return a type that itself exposes three members:

bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); // TResult can also be void

As an example of this, Task’s GetAwaiter method returns a value of type TaskAwaiter:

public struct TaskAwaiter : ICriticalNotifyCompletion

and that’s what enables awaiting the Task.  This is a simplification, but in short the OnCompleted method registers the Action as a continuation onto the Task (e.g. with ContinueWith), such that when the task completes, it will cause the compiler-generated state machine around the await to pick back up where it left off.

The title of this post is “await anything;”, so let’s see how we can await things besides Task and Task<TResult>.  To do that, we’ll need appropriate “awaiter” types for the “awaitable” type to await.  That doesn’t mean we have to write new “awaiter” types, however.  There are really two different approaches to making something awaitable: develop a new awaiter type that exposes the right pattern, or figure out how to create a Task or Task<TResult> from the thing being awaited, and then just reuse Task or Task<TResult>’s awaiter.  For the majority of cases, the latter approach is very straightforward, so we’ll start with that.

Let’s say you want to be able to write code like:

await TimeSpan.FromMinutes(15);

in order to asynchronously pause for 15 minutes.  To do that, we can develop a 1-line GetAwaiter method for TimeSpan:

public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
{
    return TaskEx.Delay(timeSpan).GetAwaiter();
}

That’s it.  Or let’s say we like waiting for periods of time so much, that we want to simply this down to just:

await 15000; // in milliseconds

No problem, we can do that with another one-line awaiter:

public static TaskAwaiter GetAwaiter(this Int32 millisecondsDue)
{
    return TimeSpan.FromMilliseconds(millisecondsDue).GetAwaiter();
}

Let’s say we like waiting for time-like things so much that we want to be able to wait until a particular date/time, ala

await DateTimeOffset.UtcNow.AddMinutes(1);

Again, piece of cake:

public static TaskAwaiter GetAwaiter(this DateTimeOffset dateTimeOffset)
{
    return (dateTimeOffset - DateTimeOffset.UtcNow).GetAwaiter();
}

Tired of time?  Alright.  The GetAwaiter function for Task allows you to wait for a single task, how about enabling waiting for an enumerable of tasks so that you can write code like:

await from url in urls select DownloadAsync(url);

Easy peasy:

public static TaskAwaiter GetAwaiter(this IEnumerable<Task> tasks)
{
    return TaskEx.WhenAll(tasks).GetAwaiter();
}

All of the examples thus far were one-liners because we already have a function that takes the input to the extension method and produces a task from it.  However, with just a few more lines, you can convert almost anything that has some notion of future completion into a task, through the TaskCompletionSource<TResult> type.  If you can express your need by completing the statement “I want to await until …” or “I want the await to complete when …”, this is likely a good approach for you.

As an example, consider wanting to spin up another process and then asynchronously wait for that process to complete, e.g.

await Process.Start(“Foo.exe”);

You could do that with a GetAwaiter method like the following:

public static TaskAwaiter<int> GetAwaiter(this Process process)
{
    var tcs = new TaskCompletionSource<int>();
    process.EnableRaisingEvents = true;
    process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
    if (process.HasExited) tcs.TrySetResult(process.ExitCode);
    return tcs.Task.GetAwaiter();
}

Or maybe you want to asynchronously wait until cancellation is requested, e.g.

await cancellationToken;

That could be done with a GetAwaiter like the following:

public static TaskAwaiter GetAwaiter(this CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    Task t = tcs.Task;
    if (cancellationToken.IsCancellationRequested) tcs.SetResult(true);
    else cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
    return t.GetAwaiter();
}

You get the idea.

The second approach to making an awaitable type is to implement a custom awaiter.  This could either be a separate type that’s returned by GetAwaiter and that exposes the IsCompleted/OnCompleted/GetResult members, or it could be a GetAwaiter method that returns “this”, with IsCompleted/OnCompleted/GetResult also exposed on the awaitable type.  You’d typically go this route if you can’t express your desire as “I want the await to complete when…”, but rather as “When the await completes, I want to continue executing …”, filling in the blank for that “…”.  In particular, you’d need to use this approach if you need full control over how (rather than when) the “Action continuation” delegate is invoked.

Imagine, for example, that you wanted to launch some work to run on the ThreadPool.  This work would compute a string and then store the result into a control on your UI.  To modify the control, you need to be on the UI thread, so you somehow need to transition to the UI thread to do that work.  If this were, for example, a Windows Forms application, we could accomplish this by building an awaiter for a Windows Forms Control.  That would allow us to write code like:

ThreadPool.QueueUserWorkItem(async delegate
{
    string text = ComputeString();
    await button1;
    button1.Text = text;
});

We want the operation of awaiting the button1 to transition to the UI thread and then continue the execution there.  We can do that with an implementation like the following:

public static ControlAwaiter GetAwaiter(this Control control)
{
    return new ControlAwaiter(control);
}

public struct ControlAwaiter : INotifyCompletion
{
    private readonly Control m_control;

    public ControlAwaiter(Control control)
    { 
        m_control = control;
    }

    public bool IsCompleted
    { 
        get { return !m_control.InvokeRequired; }
    }

    public void OnCompleted(Action continuation)
    { 
        m_control.BeginInvoke(continuation); 
    }

    public void GetResult() { }
}

You can also combine these approaches, such as by writing a custom awaiter which wraps the awaiter for a task, layering on additional functionality.  For example, culture information is not flowed by default as part of ExecutionContext, which is the standard .NET mechanism for transferring important environmental information across asynchronous invocations.  What if we wanted to make it easy to flow culture?  Imagine the following syntax for awaiting a task with the flow of culture:

await task.WithCulture();

We could enable that with code like the following:

public static CultureAwaiter WithCurrentCulture(this Task task)
{
    return new CultureAwaiter(task);
}

public class CultureAwaiter : INotifyCompletion
{
    private readonly TaskAwaiter m_awaiter;
    private CultureInfo m_culture;

    public CultureAwaiter(Task task)
    {
        if (task == null) throw new ArgumentNullException("task");
        m_awaiter = task.GetAwaiter();
    }

    public CultureAwaiter GetAwaiter() { return this; }

    public bool IsCompleted { get { return m_awaiter.IsCompleted; } }

    public void OnCompleted(Action continuation)
    {
        m_culture = Thread.CurrentThread.CurentCulture;
 
        m_awaiter.OnCompleted(continuation);
    }

    public void GetResult()
    {
        Thread.CurrentThread.CurrentCulture = m_culture;
        m_awaiter.GetResult();
    }
}

This awaiter implementation wraps a TaskAwaiter, and this implementation's IsCompleted, OnCompleted, and GetResult members delegate to the contained TaskAwaiter’s.  On top of that, though, the implementation captures the current culture in OnCompleted and then restores it in GetResult.

By now, it should be obvious that there are loads of interesting possibilities here.  I look forward to seeing all the interesting and useful awaiters you come up with.  Just keep in mind that while there are plenty of “cool” things you can do, code readability and maintainability is really important, so make sure that the coolness isn’t trumped by lack of clarity about the code’s meaning.

[NOTE 4/09/2012: This blog post was originally based on the original Async CTP released in Oct 2010.  Visual Studio 11 Beta released in Feb 2012 uses an updated awaiter pattern, and this post has now been updated to conform to that pattern.]

Leave a Comment
  • Please add 3 and 7 and type the answer here:
  • Post
  • Hi toub,

    Why isn't there an interface with methods (GetAwaiter, BeginAwait and EndAwait) to implement on our types that needs an awaiter?

  • once again excellent stuff .. looks like I will be having fun over the weekend!!!!

    Cheers guys .....

  • There is a race condition here:

    process.Exited += (s, e) => tcs.SetResult(process.ExitCode);

    if (process.HasExited) tcs.SetResult(process.ExitCode);

    It might detect exit twice.

  • tobi, oops, in my haste I wrote Set instead of TrySet.  Fixed above.

  • Tridex, glad you like it!  Enjoy your weekend :)

  • Paul, we spent a long time evaluating IAwaitable before deciding against it. Here are some of the factors.

    (1. PRO) If would be good for "Await" to be like "For Each". For Each first looks to see if its argument satisfies the enumerable pattern. If not, it looks for an interface IEnumerable<T> / IEnumerable and uses that. It would be clean if Await did the same thing.

    (2. CON) At the moment we have a large set of Task-related combinators, e.g. Task.WaitAll and Task.WaitAny, that operate upon tasks. To make IAwaitable usable, we'd have to add extra overloads of them take IAwaitable as well.

    (3. -) Actually, not all awaitables are peers. We wouldn't want "Task.WhenAll(Task.Run(...), Task.Yield())" to typecheck. It doesn't make sense. We kind of like that the type system keeps them apart. (Of course it still could even with IAwaitable, in the same way that you can For Each over things that aren't IEnumerable).

    (4. CON) Users would have to decide all the time whether their API surface area should deal in Tasks or IAwaitables -- a decision that most would have to make without it actually benefitting them.

    (5. CON) We'd have to decide async methods should have return type Task or IAwaitable or both. If Task, then it adds to user confusion about whether their signatures should have Task or IAwaitable. If IAwaitable, then it cuts users off from all the existing Task-based APIs. If both, then it adds to user confusion about what their async method should return.

    (6. PRO) If we created an IEnumerable, then users could write their own libraries of combinators that operate over anyone's awaitable stuff.

    (7. CON) At this stage, we have only one and a half good examples of things that are awaitable -- namely, Task and maybe IObservable. It'd be a mistake to create a fundamental framework type like IAwaitable with so few uses of it.

    (8. CON) IAwaitable feels a bit like IObservable.

    I think it was 5+7 that were the biggest factors in our decision.

    --

    Lucian Wischik, VB language PM

  • I would add another con: There is a perfectly valid workaround to not having an interface and achieving basically the same thing: Task.

  • I love what I'm seeing, but I'm a practitioner and not an early adopter.  I think you'd get more adoption from practitioners if documentation for TaskEx was up on MSDN library somewhere.  (I think I'd use it in my personal PowerShell and data migration code, of course not production uses).  Any ETA on when there will be basic docs on MSDN for the new CTP types like TaskEx?  Would we have to wait for a .NET 5 beta?

  • Hi yzorg-

    Thanks for the feedback and suggestion.  It is likely that you'll need to wait for a beta release in order to get such documentation, but we'll see if there's something we can do in the meantime (e.g. putting together a CHM file that documents the new members).

  • This is awesome stuff. I can already imagine how easy it would be to write coroutines using async/await.

  • Hi,

    This post is very interesting and show the power of using a pattern instead of concrete types.

    The thing that bugs me is that the last two ideas of await button and await culture seem like an abuse to me.

    I can read and understand what await DownloadAsync means but reading await button is not logical to me. I can see why it working but in order to understand that you have to know the internal implementation of both Awaiter pattern and SynchornizationContext, both of which should be hidden if used correctly.

    As a comparison - I can add operator ++ to Thread which change it's priority but it will just abuse operator overloading.

    That's only my opinion :)

    Thanks for the great work of finally bring great async pattern that works even on UI code.

    Ido

  • Hi Ido-

    I'm glad you like the post, and thanks for the feedback.  To be clear, neither of those control or culture examples actually ship in the Framework, and I was simply showing the kinds of things that are possible with the pattern.  Whether you choose to do this in your own code is entirely up to you :)

  • Can we write a custom awaiter for IEnumerable<Task<T>> or IEnumerable<Task> that executes continuation after each task completed? for example:

    await arrayOfTwoTasks;

    Console.WriteLine("Done"); // this will be executed two times (after each task completes)

  • @alrz: No, if you want to do that you should use ContinueWith and the delegate to be invoked when each task completes, e.g.

    foreach (var task in tasks) {

       task.ContinueWith(t => Console.WriteLine("done"));

    }

Page 1 of 1 (14 items)