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.

  • Oh, and another comment, related to some of the other suggestions:

    I definitely prefer the current linear model over a block-oriented approach suggested by some of the other commenters.  For me, the reason this adds value is that it offers a way to write and read the code that expresses the sequence of statements accurately but with a minimum of overhead.

    If a block-oriented syntax were desirable, it would be easy enough today to just write a library that uses methods accepting delegate references. We'd have no need for language support at all. It can be done as the language stands now, and would be just as awkward as incorporating a block-oriented approach into the language would be.

  • (My apologies if this shows up twice…the blog said the message had been posted already, but the page was behaving a little oddly — the text of the comment didn't get cleared — and my comment never appeared. So I'm trying again, just in case).

    Hmmm…I'm just catching up on some of the other recent posts in this blog (I'm behind because of a MSDN/browser incompatibility issue that prevents me from seeing the articles in my usual browser). Last month, there was this article:

    blogs.msdn.com/.../ambiguous-optional-parentheses-part-three.aspx

    In it, Eric discusses how they might identify and deal with potential ambiguities introduced in new features.

    While I like the standalone "await" keyword, now I'm wondering if it doesn't introduce exactly the kind of ambiguity being discussed here. And if so, maybe that's why "async" was introduced as well (since it would remove the ambiguity)?

  • My proposal is to make the keyword names express the programmer intention and not the underlined technical meaning. Therefore I would propose to remove the "async" keyword from the method signature (because I don't see the need in it neither from compiler nor from programmer perspective), instead I would use "async" in place of await to express that that method will be invoked asynchronously.

    In general this idea tries to align the feature usage with "yield" feature usage.

  • "Therefore I would propose to remove the "async" keyword from the method signature (because I don't see the need in it neither from compiler nor from programmer perspective), instead I would use "async" in place of await to express that that method will be invoked asynchronously."

    As I've mentioned in my comments, I agree that I don't see the need for "async". But that may just be because there's a need I don't see, rather than there not being a need.

    More to the point here though, is that "async" isn't a suitable alternative to "await", for an important reason already stated previously: the invocation of the target of the special word ("await" or "async" or whatever) is not actually guaranteed to be executed asynchronously. The "async" keyword is definitely misleading in that context.

  • You are instructing the system to perform an action *when* something is done, therefore I think that "when" is the appropriate keyword.  

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

  • You are instructing the system to perform an action *when* something is done, therefore I think that "when" is the appropriate keyword.  

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

  • @fatcat1111

    I like your suggestion. It is simple and explicit.

    I would be fine with replacing the "await" with "async" and removing async from the method declaration aus pete suggested.

  • As I mentioned, I think "await" is fine. But, having had more time to consider the question, and keeping in mind the desire for this to reflect the "what" rather than the "how", I think "await" and all the similar syntaxes aren't quite right.

    At the moment, my preferred syntax would be:

     • No "async" at all (i.e. method declaration is untouched…"async" is inferred from the body, like iterators)

     • The word "future" instead of "await"

    This is more declarative than imperative. That is, rather than telling the compiler "here's where we wait", you're telling the compiler that you want to assign the _future_ value of something. As in, "I know the value's not necessarily going to be available right this minute…I want whatever the value of it is in the future though".

    I saw another suggestion that argues in favor of no new keywords at all, and for the compiler to infer from usage what to do. That actually seems feasible to me — the plain vanilla scenario would be "assigning a Task<T> to a T would infer an 'await' [or 'future' or whatever it's called]" — but I'm sure I'm being naïve about it. :)

  • Who doesn’t like to look sexy and be noticed for their superior flavor in style? Every female looks and feels sexy when she wears high heels. One man who helped to reinforce the trend of high heels is Christian Louboutin Pumps.

    One of his popular shoes is the Christian Louboutin Pumps Rolando. Christian Louboutin Pumps Rolando is found in different styles. The Christian Louboutin Pumps Rolando Hidden-Platform is an elegant chic shoe that can be damaged to correct dos. Christian Louboutin Pumps Rolando has a curved toe and a 5 shuffle heel. It is offered in sinister red and black patent leather. Christina Aguilera has been dappled tiresome the dark red patent leather Christian Louboutin Pumps Rolando on different occasions.

    Christian Louboutin Pumps are also untaken in a suede kill. The Christian Louboutin Pumps  hoary suede ‘Rolando’ platform pumps have has a refined, sophisticated air about it. It has a giving older suede greater, a trenchant toe and leather lining. It features a ¾” roofed front platform and a 4½” roofed heel. It is also offered in black.

    If you want a branded stylish piece of footwear that makes your legs look long and exquisite, get yourself a twosome of Christian Louboutin Pumps Rolando.

    www.christianlouboutinpumps.org

  • For me the best term is "watch" like in watchman or watchtower. For me the statement says "continue the code while I watch that variable here"

  • Hi, please explain why need "async" modifier, because we don't need "iterator" modifier when using "yield return/break".

    Why not just generate Task when the method body has await expressions and returns Task/Task<T>/void?

  • I like 'yield until', which someone proposed earlier in the comments.  'Yield' is nice because it captures the concept of yielding control to the calling function without implying blocking.  'Until' is nice too, because it focuses the reader's attention on the asynchronous method, not on the code beyond it.

    (I'm not a fan of 'continue' or 'after' in this context.  Continue is too reminiscent of the existing continue statement, and 'after' focuses your attention on the code that follows the asynchronous method.)

  • @Beevik: You don't like continue because of the existing continue statement, but you do like yield?  I don't see how either is better than the other in that regard.

  • I'm also in favor to remove 'async' completely to be coherent with how iterators work.

    As for 'await', as your usability search proved, it's quite confusing the first time. However, and it's the case for any new feature, once you got accustomed to it that's not really a problem anymore.

    That being said, I would prefer the keyword 'complete' instead of 'await' or any 'yield'/'continue' combination. We want that task completed, whether it already was or will be. I think the implied "wait if you have to" is quite clear with this word.

  • Just thought I'd remind people that Eric said in the original post "At a later date I’ll discuss the reasons behind requiring the async modifier rather than inferring it."

    May as well leave comments about removing that part entirely until that blog post...

Page 7 of 11 (161 items) «56789»