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.

  • What about symbol instead of 'await'. Something like <> or ><.

    var document = <> FetchAsync(<> GetIdAsync());

  • I think 'async' and 'await' are perfect.  They're short and simple, they comprise only one keyword apiece (two for the entire feature set), and they both start with 'a', which implies they work together.  'yield for' takes two completely unrelated keywords and implements them in a non-intuitive way for which neither was intended; 'continue' is already used in a synchronous context and should not be re-used for something quite the opposite.  'after' doesn't even make since to me; you're not executing the method "after" anything, you're calling it immediately and then doing other stuff while you wait for it to finish executing.  I say stick with the a's.

  • Read most but not all comments. One of the first thing that occurred to me was "yield pending". Apologies if it's already been put out there.

  • Typo in "which is then somehow asynchronously fetches": spurious "is".

    Awaiter, there's a bug in my spaghetti code.

  • (Too many comment here, so I didn't read them all, my suggestion might have already been given.)

    Why not, instead of "await", just use .... "async" again ?

  • (Too many comment here, so I didn't read them all, my suggestion might have been already been given.)

    Why not, instead of "await", just use .... "async" again ?

  • Since the function is being modified to return a task, why not use "task" as the keyword?

    "finish" would work well instead of "await", since you need the function to be completed, but you don't necessarily need to wait.

  • Eric! How about "await return". It makes it clearer that the method returns here, and mirrors "yield return" exactly.

    Cheers,

    Pete

  • I think "continue" or "continue after" or "yield" are far, far, far better choices than "await".

    "await" simply reeks of blocking and I would be fascinated to understand why the other choices were all discarded.

  • I very much like "yield until".  Await feels like a blocking call.

    yield, tell me that control flow is returning to the caller, just like in an iterator block, and 'until' is telling me when control is returning to this block again "go away until this thing is done".

  • "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"

    There's nothing wrong with people's intuition or ability to understand terms in context.  If the keyword is ambiguous to many people, it is a poorly chosen keyword.  If the keyword's meaning seems to be the opposite of what it is, then there couldn't be a WORSE choice for the keyword.

    If I understand correctly, the await keyword does two things in a particular sequence: it starts the code that follows in an async task and THEN returns to the calling method immediately.  This is a fairly complex action that warrants a descriptive keyword.

    I suggest using the keyword ReturnAfterStarting rather than await.  Sure it is long, but so what?  We've got hundred gigabyte hard disks, compilers and IntelliSense; we can deal with a few more bytes in the source for the sake of clarity.  Let's do away with keywords that can be understood only after you've scratched your head for hours and done multiple trial-and-error tests.

    My first thought was ReturnFire, but may be too steeped in English idioms to be suitable.

  • I agree with all those that were confused with the the name used for this  new  "await" operator. Plato has said that we are at the beginning of wisdom when we start visiting and exploring the names.

    So my proposal is to use the word "anathesis" [of greek origin] which means "assign a job and return".

    The fact that is not well known to English speaking people is good because they will oblige most of the readers to look for its meaning and as a result they will not be confused by the preconceptions they already have. On the other hand, the "anathesis" operator  will pair perfectly with the "async" keyword which is also of greek origin.

  • var document = continuation FetchAsync(urls[i]);

  • Why two different word - await and async. Couldn't people just get by with one - async - for both methods and calls? BTW, async in calls in by fa the least confusing (compared to await at least :)

  • After reading through a number of these comments, I tend to agree with StarBright and Jon Skeet for the "await" keyword.  I personally prefer "continue with" (StarBright's reasoning makes a ton of sense to me), but I could live with "continue after".

    As for the "async" keyword, it really does seem counter intuitive, but I like Roland's idea of "comethod", or perhaps "coop".

Page 10 of 11 (161 items) «7891011