Asynchronous Programming in C# 5.0 part two: Whence await?

Asynchronous Programming in C# 5.0 part two: Whence await?

Rate This

I want to start by being absolutely positively clear about two things, because our usability research has shown this to be confusing. Remember our little program from last time?

async void ArchiveDocuments(List<Url> urls)
{
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    if (archive != null)
      await archive;
    archive = ArchiveAsync(document);
  }
}

The two things are:

1) The “async” modifier on the method does not mean “this method is automatically scheduled to run on a worker thread asynchronously”. It means the opposite of that; it means “this method contains control flow that involves awaiting asynchronous operations and will therefore be rewritten by the compiler into continuation passing style to ensure that the asynchronous operations can resume this method at the right spot.” The whole point of async methods it that you stay on the current thread as much as possible. They’re like coroutines: async methods bring single-threaded cooperative multitasking to C#. (At a later date I’ll discuss the reasons behind requiring the async modifier rather than inferring it.)

2) The “await” operator used twice in that method does not mean “this method now blocks the current thread until the asynchronous operation returns”. That would be making the asynchronous operation back into a synchronous operation, which is precisely what we are attempting to avoid. Rather, it means the opposite of that; it means “if the task we are awaiting has not yet completed then sign up the rest of this method as the continuation of that task, and then return to your caller immediately; the task will invoke the continuation when it completes.

It is unfortunate that people’s intuition upon first exposure regarding what the “async” and “await” contextual keywords mean is frequently the opposite of their actual meanings. Many attempts to come up with better keywords failed to find anything better. If you have ideas for a keyword or combination of keywords that is short, snappy, and gets across the correct ideas, I am happy to hear them. Some ideas that we already had and rejected for various reasons were:

wait for FetchAsync(…)
yield with FetchAsync(…)
yield FetchAsync(…)
while away the time FetchAsync(…)
hearken unto FetchAsync(…)
for sooth Romeo wherefore art thou FetchAsync(…)

Moving on. We’ve got a lot of ground to cover. The next thing I want to talk about is “what exactly are those ‘thingies’ that I handwaved about last time?”

Last time I implied that the C# 5.0 expression

document = await FetchAsync(urls[i])

gets realized as:

state = State.AfterFetch;
fetchThingy = FetchAsync(urls[i]);
if (fetchThingy.SetContinuation(archiveDocuments))
  return;
AfterFetch: ;
document = fetchThingy.GetValue();

what’s the thingy?

In our model for asynchrony an asynchronous method typically returns a Task<T>; let’s assume for now that FetchAsync returns a Task<Document>. (Again, I’ll discuss the reasons behind this "Task-based Asynchrony Pattern" at a later date.) The actual code will be realized as:

fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
state = State.AfterFetch;
if (fetchAwaiter.BeginAwait(archiveDocuments))
  return;
AfterFetch: ;
document = fetchAwaiter.EndAwait();

The call to FetchAsync creates and returns a Task<Document> - that is, an object which represents a “hot” running task. Calling this method immediately returns a Task<Document> which is then somehow asynchronously fetches the desired document. Perhaps it runs on another thread, or perhaps it posts itself to some Windows message queue on this thread that some message loop is polling for information about work that needs to be done in idle time, or whatever. That’s its business. What we know is that we need something to happen when it completes. (Again, I’ll discuss single-threaded asynchrony at a later date.)

To make something happen when it completes, we ask the task for an Awaiter, which exposes two methods. BeginAwait signs up a continuation for this task; when the task completes, a miracle happens: somehow the continuation gets called. (Again, how exactly this is orchestrated is a subject for another day.) If BeginAwait returns true then the continuation will be called; if not, then that’s because the task has already completed and there is no need to use the continuation mechanism.

EndAwait extracts the result that was the result of the completed task.

We will provide implementations of BeginAwait and EndAwait on Task (for tasks that are logically void returning) and Task<T> (for tasks that return a value). But what about asynchronous methods that do not return a Task or Task<T> object? Here we’re going to use the same strategy we used for LINQ. In LINQ if you say

from c in customers where c.City == "London" blah blah blah

then that gets translated into

customers.Where(c=>c.City=="London") …

and overload resolution tries to find the best possible Where method by checking to see if customers implements such a method, or, if not, by going to extension methods. The GetAwaiter / BeginAwait / EndAwait pattern will be the same; we’ll just do overload resolution on the transformed expression and see what it comes up with. If we need to go to extension methods, we will.

Finally: why "Task"?

The insight here is that asynchrony does not require parallelism, but parallelism does require asynchrony, and many of the tools useful for parallelism can be used just as easily for non-parallel asynchrony. There is no inherent parallelism in Task; that the Task Parallel Library uses a task-based pattern to represent units of pending work that can be parallelized does not require multithreading.

As I've pointed out a few times, from the point of view of the code that is waiting for a result it really doesn't matter whether that result is being computed in idle time on this thread, in a worker thread in this process, in another process on this machine, on a storage device, or on a machine halfway around the world. What matters is that it's going to take time to compute the result, and this CPU could be doing something else while it is waiting, if only we let it.

The Task class from the TPL already has a lot of investment in it; it's got a cancellation mechanism and other useful features. Rather than invent some new thing, like some new "IFuture" type, we can just extend the existing task-based code to meet our asynchrony needs.

Next time: How to further compose asynchronous tasks.

  • How about:

       var document = GOSUB FetchAsync(urls[i]);

    Yeah, I have terrible ideas. I'll see myself out...

  • Eric, I have a bit of criticism.

    When you want to clarify a term you *_should not_* do it with a sentence as long and complex as:

    it means “this method contains control flow that involves awaiting asynchronous operations and will therefore be rewritten by the compiler into continuation passing style to ensure that the asynchronous operations can resume this method at the right spot.”

    Even at the cost of some accuracy. Prefer shorter bite size sentences. I am now much, much more confused than I was. Also you should not explain this from the POV of the compiler (which is what you are used to) but from the POV of a user. understanding how a feature is compiled *is not* Introductory by any means, again, even if it accurately depicts the behavior.

    I know explaining things you work with daily from start is difficult, I deal with that myself and make such errors all the time. This is why I wrote you this criticism.

    BTW, Thanks for the blog, read regularly.

  • It just occurred to me that this type of control flow is actually reminiscent of INTERCAL:

    en.wikipedia.org/.../COMEFROM

    Maybe instead of "await", the syntax should be "come from"!

  • I'm glad Eric posted on this today, my brain kept me up most of the night trying to work out whether await was short for async-wait, and how waiting could be synonymous with NOT waiting. That and pondering whether/why the async keyword was even necessary.

    Given that I'm not clear about the meaning/necessity of async, I can't really argue with it, but here are a few more suggestions to add to the list:

    yield while  (my favourite, and look!, no new keywords)

    pending

    on done resume next (wink at vb6)

    Shame we can't vote here.. if you feel strongly enough about it, try <a href="programmers.stackexchange.com/.../a>.

  • Just saw this question on Don Syme's blog, thought this might be a good place to ask it too:

    "Does this mean C# and VB will now also be relying upon tail call elimination in the VM?"

  • Eric, I hope you're still reading these comments. I'd like to suggest:

       var document = yield while FetchAsync(urls[i]);

    `yield while` perfectly captures the semantic meaning of the code -- it yields while FetchAsync runs, and (just like the similar iterator construct) regains control once the value is processed. `yield until` would work also, but including the word `yield`, in my opinion, is a must -- the similarity to iterators is too great not to use the same keyword for both. To quote John Leidegren from Stack Overflow, you are *yielding control*, rather than yielding values. (And to top it off, it reuses existing keywords)

    Just like in your usability tests, the `await` keyword was a major barrier to me understanding the concept. I'm very curious why `yield ____ task;` and its variants were rejected, if you'd be so kind as to respond with a sentence or two.

  • The "async" modifier confused me more than the await operator. Per your quote in the article:

    " They’re like coroutines: async methods bring single-threaded cooperative multitasking to C#."

    How about "comethod" instead of "async" for the modifier.

  • @John.

    "yield while" gets my vote.

  • I perfer to think of "await" as "when the tasks completes" so to me the keyword "when" makes more sense than await.  It's also one less letter to type!

    Task<string> data = DownloadAsync(url);

    string str = when data;

    How about renaming await to when?

  • @Data - you missed the snark, that wasn't a serious suggestion.   C# doesn't need the distinction between let! and do!, since expression values get silently discarded anyway.   Since the proposed await keyword forms an expression rather than a binding construct, in retrospect I presume it can just be used in:

    using (var foo = await Bar()) {...}

  • I'll second the vote for "deferred" instead of "async". As for "await", "yield until" or (second choice) "yield while" seem better.

  • @John I was just about to offer the exact same suggestion. It captures the intent of the operation rather than its implementation.

    +1 yield while

  • Why not just make it "async DoWork(...)", since you're basically saying "do this work asynchronously"

  • I like 'continue after' or 'defer until' in place of 'await'. They give the needed feel that the program is going to go off and do something else and then come back here when this step completes. The jury is still out on async. I keep flip-flopping on whether it makes sense in my gut. Regardless of the symantics, though, a beautiful language feature to have.

  • +1 yield while

Page 4 of 11 (161 items) «23456»