waffle

Waffle is a weblog.
The author of Waffle, some guy in Sweden, also occasionally writes stmts.net.

The Long-awaited async

I’ve been wracking my mind trying to figure out how to best summarize the new async/await feature in C# 5.

A short example is this:

async Task<Tweet[]> GetTweetsOfAllFollowers(string query, TwitterAPI api) {
    var matchingTweets = new List<Tweet>();
    var followers = await api.GetFollowers(); // 1
    foreach(var follower in followers) {
        var tweets = await api.GetRecentTweets(follower); // 2
        var applicableTweets = tweets.Where(tw => tw.Text.Contains(query));
        matchingTweets.AddRange(applicableTweets);
    }
    return matchingTweets.ToArray();
}

The interesting thing about this is how it runs. It starts out at the top, naturally, and then runs to point 1. At that point, the rest of the method, right up until the end, is packaged up as a continuation. The GetTweetsOfAllFollowers method returns and posts a call to api.GetFollowers() to the current synchronization context, meaning in most cases to the message queue on the current thread.

Right after that, the method will run and kick off its own asynchronous processing. When it’s done, it’ll pick up after point 1 and continue running through the function. Each time it encounters point 2 (which may be never, if you have no followers), it’ll do the same thing all over again.

Some pre-emptive answers:

  • Task<T> is a future. It’s an existing type that can carry a result, be cancelled and be faulted (and then carry a specific exception).
  • await doesn’t block; it does just the opposite. Instead of holding the thread still until the method it awaits returns, it lets the thread do other stuff — yields, you might say — until the method it awaits returns.
  • In some cases, await can run the method inline. Let’s say GetFollowers cached its results; it’d return those synchronously and just carry on.
  • The compiler assembles the Task<T> return value for you, while you provide the actual value that it will eventually resolve.
  • The compiler contorts the code for you to work with identical semantics but remain technically able to be passed as a continuation.
  • You can use, throw and catch exceptions pretty much like you usually would. The compiler handles wrapping and unpacking the exception into the Task.
  • An async method isn’t flagged as such in the metadata; it merely is one of many methods to return a Task<T>.
  • And finally, what you all are thinking: yes, if you wrote a single-threaded program with just async/await in it instead of threads or thread pools, it’d effectively work pretty much like node.js.

For more technical detail, I think I’ll link to Lucian Wischik’s technical walk. It’s the sort of thing you love because it involves potshots at competitors in PowerPoint presentations by Microsoft employees where the Microsoft employees are right, the Microsoft employees aren’t marketing and the potshot is about clumsy technical reasoning. (That’s actually just a very minor part. Most of it is gritty details and a better explanation than I can muster. I highly recommend it.)

I will also link to Eric Lippert’s deep but riveting tease-a-thon: Continuation Passing Style Revisited, parts one, two, three, four and five.

But finally, why post about this? Isn’t this just Microsoft catching up to computer science? In a way, yes. Not only could you have written this last week in Scheme, you could have written it last week in C#, assuming you’re willing to unravel the structure of your code into what the process requires. The point is that this is the first major language that I’m aware of to actually add support to unfurl your normal iterative code into callbacks using compiler support. In a world where the conventional wisdom posits that Microsoft is perpetually ripping off a Java that can’t even seem to grow some closures, I thought they deserved some credit.

Comments

  1. Ryan Dahl (creator of node.js) has said that callbacks are strictly better than continuations, because they explicitly show what parts of the code are happening later. It’s a factor that you can’t ignore completely because async is non-atomic, even with a single thread. V8 actually supports continuations (with the JS yield operator), but the stock node.js APIs will probably never use them.

    It also seems like callbacks are more powerful, because you can set up any number of them in parallel, and do whatever you want while waiting for each of them to complete. In the example above, each iteration of the loop waits in sequence. (Perhaps there is an easy way to solve this with continuations–I don’t know.)

    That said, I agree that deeply nested callbacks can get pretty hairy.

    By Ben · 2010.10.31 16:16

  2. Ben: First, you don’t have to await just a single call, you can also collect them and await the first (any) or all of them, which seems to me to allow for arbitrary complexity (for better or worse). Composability like this is laudable.

    Secondly, callbacks are very useful, and C# has had support for doing them inline at all since 2.0 (with inline “delegate”s) and well since 3.0 (with lambdas). I’m unsure if you were pitting C# against callbacks or continuations against callbacks, but in the C# case, it already handles callbacks!

    The point with async/await is that the popular callback-errback pattern that many asynchronous calls follow can be supported without needing to syntactically use callbacks, just by using await and then doing something with the result (callback) or catching the exception (errback).

    Aside from doing that, the new feature also undoes the Gordian knot that is the .NET framework’s tragic history of async models. One attempt used two otherwise uncoupled methods and required both casting parameters and context values and carting around a job representation which wasn’t a future.

    The other attempt used a method and an event that were otherwise uncoupled, which is easier to wrap your head around than the first attempt, but dangerous since the callback is entirely uncoupled from the call, so you could both accidentally remove a callback by being too eager to place the next call or risk delivering more calls to the original callbacks than you intended. When this happened, it was certainly a case of bad design, but the design of the async model was worse, in that it allowed this to even happen.

    By Jesper · 2010.10.31 19:29

  3. I was comparing paradigms, not languages or frameworks. I think callbacks and continuations are both interesting, and I’m still not entirely sure which one is preferable (for typical use).

    I see that they have WhenAny and WhenAll, but that doesn’t handle arbitrary actions in the general sense. In particular, it sounds like this caused the designers considerable hardship with regards to return values. With continuations, fire-and-forget is a special case, whereas with callbacks, it’s the most basic. That might have been some of the complexity to which ry was referring.

    BTW, thanks for the great blog.

    By Ben · 2010.10.31 23:36

  4. “The point is that this is the first major language that I’m aware of to actually add support to unfurl your normal iterative code into callbacks using compiler support.”

    Um, hate to steal C#’s thunder but Scala, a JVM language, introduced this capability (delimited continuations) with its 2.8 release.

    By Tom Crockett · 2010.11.03 03:40

  5. Just making sure, it turns:

    a; for (int i = 0; ...; i++) { b;
    something_simulating_a_call_with_a_callback;
    c; d;
    the_same_sort_of_call;
    e; } f; 
    

    into two levels deep nested callbacks, including handling the semantics as close to the original language semantics as possible, including handling exceptions, proper flow control (going e, then b), having the appropriate local variables in scope and with sensible values (like the right value of i in the loop)?

    I wouldn’t put it past Scala to do that, actually. Assuming it does that, I suppose I could hide under the “major language” quibble, but I won’t. I still think it’s a rather neat credit that they started designing this feature at approximately the same time as Scala (2.8 came out in July and the await/async effort has been going on for over a year), especially for such a stodgy group of languages as C# inhabits.

    By Jesper · 2010.11.03 20:51

Sorry, the comment form is closed at this time.