Asynchrony in C# 5 Part Six: Whither async?

Asynchrony in C# 5 Part Six: Whither async?

Rate This
  • Comments 37

A number of people have asked me what motivates the design decision to require any method that contains an "await" expression to be prefixed with the contextual keyword "async".

Like any design decision there are pros and cons here that have to be evaluated in the context of many different competing and incompossible principles. There's not going to be a slam-dunk solution here that meets every criterion or delights everyone. We're always looking for an attainable compromise, not for unattainable perfection. This design decision is a good example of that.

One of our key principles is "avoid breaking changes whenever reasonably possible". Ideally it would be nice if every program that used to work in C# 1, 2, 3 and 4 worked in C# 5 as well. (*) As I mentioned a few episodes back, (**) when adding a prefix operator there are many possible points of ambiguity and we want to eliminate all of them. We considered many heuristics that could make good guesses about whether a given "await" was intended as an identifier rather than a keyword, and did not like any of them.

The heuristics for "var" and "dynamic" were much easier because "var" is only special in a local variable declaration and "dynamic" is only special in a context in which a type is legal. "await" as a keyword is legal almost everywhere inside a method body that an expression or type is legal, which greatly increases the number of points at which a reasonable heuristic has to be designed, implemented and tested. The heuristics discussed were subtle and complicated. For example, var x = y + await; clearly should treat await as an identifer but should var x = await + y do the same, or is that an await of the unary plus operator applied to y? var x = await t; should treat await as a keyword; should var x = await(t); do the same, or is that a call to a method called await?

Requiring "async" means that we can eliminate all backwards compatibility problems at once; any method that contains an await expression must be "new construction" code, not "old work" code, because "old work" code never had an async modifier.

An alternative approach that still avoids breaking changes is to use a two-word keyword for the await expression. That's what we did with "yield return". We considered many two-word patterns; my favourite was "wait for". We rejected options of the form "yield with", "yield wait" and so on because we felt that it would be too easily confused with the subtly different continuation behaviour of iterator blocks. We have effectively trained people that "yield" logically means "proffer up a value", rather than "cede flow of control back to the caller", though of course it means both! We rejected options containing "return" and "continue" because they are too easily confused with those forms of control flow. Options containing "while" are also problematic; beginner programmers occasionally ask whether a "while" loop is exited the moment that the condition becomes false, or if it keeps going until the bottom of the loop. You can see how similar confusions could arise from use of "while" in asynchrony.

Of course "await" is problematic as well. Essentially the problem here is that there are two kinds of waiting. If you're in a waiting room at the hospital then you might wait by falling asleep until the doctor is available. Or, you might wait by reading a magazine, balancing a chequebook, calling your mother, doing a crossword puzzle, or whatever. The point of task-based asynchrony is to embrace the latter model of waiting: you want to keep getting stuff done on this thread while you're waiting for your task to complete, rather than sleeping, so you wait by remembering what you were doing, and then go do something else while you're waiting. I am hoping that the user education problem of clarifying which kind of waiting we're talking about is not insurmountable.

Ultimately, whether it is "await" or not, the designers really wanted it to be a single-word feature. We anticipate that this feature will potentially be used numerous times in a single method. Many iterator blocks contain only one or two yield returns, but there could be dozens of awaits in code which orchestrates a complex asynchronous operation. Having a succinct operator is important.

Of course, you don't want it to be too succinct. F# uses "do!" and "let!" and so on for their asynchronous workflow operations. That! makes! the! code! look! exciting! but it is also a "secret code" that you have to know about to understand; it's not very discoverable. If you see "async" and "await" then at least you have some clue about what the keywords mean.

Another principle is "be consistent with other language features". We're being pulled in two directions here. On the one hand, you don't have to say "iterator" before a method which contains an iterator block. (If we had, then "yield return x;" could have been just "yield x;".) This seems inconsistent with iterator blocks. On the other hand... let's return to this point in a moment.

Another principle we consider is the "principle of least surprise". More specifically, that small changes should not have surprising nonlocal results. Consider the following:

void Frob<X>(Func<X> f) { ... }
...
Frob(()=> {
    if (whatever)
    {
        await something;
        return 123;
    }
    return 345;
  } );

It seems bizarre and confusing that commenting out the "await something;" changes the type inferred for X from Task<int> to int. We do not want to add return type annotations to lambdas. Therefore, we'll probably go with requiring "async" on lambdas that contain "await":

Frob(async ()=> {
    if (whatever)
    {
        await something;
        return 123;
    }
    return 345;
  } );

Now the type inferred for X is Task<int> even if the await is commented out.

That is strong pressure towards requiring "async" on lambdas. Since we want language features to be consistent, and it seems inconsistent to require "async" on anonymous functions but not on nominal methods, that is indirect pressure on requiring it on methods as well.

Another example of a small change causing a big difference:

Task<object> Foo()
{
    await blah;
    return null;
}

if "async" is not required then this method with the "await" produces a non-null task whose result is set to null. If we comment out the "await" for testing purposes, say, then it produces a null task -- completely different. If we require "async" then the method returns the same thing both ways.

Another design principle is that the stuff that comes before the body of a declared entity such as a method is all stuff that is represented in the metadata of the entity. The name, return type, type parameters, formal parameters, attributes, accessibility, static/instance/virtual/override/abstract/sealed-ness, and so on, are all part of the metadata of the method. "async" and "partial" are not, which seems inconsistent. Put another way: "async" is solely about describing the implementation details of the method; it has no impact on how the method is used. The caller cares not a bit whether a given method is marked as "async" or not, so why put it right there in the code where the person writing the caller is likely to read it? This is points against "async".

On the other hand, another important design principle is that interesting code should call attention to itself. Code is read a lot more than it is written. Async methods have a very different control flow than regular methods; it makes sense to call that out at the top where the code maintainer reads it immediately. Iterator blocks tend to be short; I don't think I've ever written an iterator block that does not fit on a page. It's pretty easy to glance at an iterator block and see the yield. One imagines that async methods could be long and the 'await' could be buried somewhere not immediately obvious. It's nice that you can see at a glance from the header that this method acts like a coroutine.

Another design principle that is important is "the language should be amenable to rich tools". Suppose we require "async". What errors might a user make? A user might have an have a method with the async modifier which contains no awaits, believing that it will run on another thread. Or the user might write a method that does have awaits but forget to give the "async" modifier. In both cases we can write code analyzers that identify the problem and produce rich diagnostics that can teach the developer how to use the feature. A diagnostic could, for instance, remind you that an async method with no awaits does not run on another thread and give suggestions for how to achieve parallelism if that's really what you want. Or a diagnostic could tell you that an int-returning method containing an await should be refactored (automatically, perhaps!) into an async method that returns Task<int>. The diagnostic engine could also search for all the callers of this method and give advice on whether they in turn should be made async. If "async" is not required then we cannot easily detect or diagnose these sorts of problems.

That's a whole lot of pros and cons; after evaluating all of them, and lots of playing around with the prototype compiler to see how it felt, the C# designers settled on requiring "async" on a method that contains an "await". I think that's a reasonable choice.

Credits: Many thanks to my colleague Lucian for his insights and his excellent summary of the detailed design notes which were the basis of this episode.

Next time: I want to talk a bit about exceptions and then take a break from async/await for a while. A dozen posts on the same topic in just a few weeks is a lot.


(*) We have violated this principle on numerous occasions, both (1) by accident, and (2) deliberately, when the benefit was truly compelling and the rate of breakage was likely to be low. The famous example of the latter is F(G<A,B>(7)). In C# 1 that means that F has two arguments, both comparison operators. In C# 2 that means F has one argument and G is a generic method of arity two.

(**) When I wrote that article I knew that we would be adding "await" as a prefix operator. It was an easy article to write because we had recently gone through the process of noodling on the specification to find the possible points of ambiguity. Of course I could not use "await" as the example back in September because we did not want to telegraph the new C# 5 feature, so I picked "frob" as nicely meaningless.

  • How about "yield void" - to reinforce that there's no value being returned.

  • IMHO await is kind of misleading, its easily mistaken as "wait till this completes..." which is essentially the opposite of what it really means.

    I'm kind of curious why the C# team chose "async" as the method prefix and not as the asynchronous expression identifier: "async Blah.Frob()" kind of implies that Frob() will be executed asynchronously. It's true though that it doesn't imply that it will come back when Frob() is finished and continue where it left off.

    Maybe "async await" would have been a good choice but thats two words and I understand the reasons to try and keep it as only one.

  • IMHO await is kind of misleading, its easily mistaken as "wait till this completes..." which is essentially the opposite of what it really means.

    I'm kind of curious why the C# team chose "async" as the method prefix and not as the asynchronous expression identifier: "async Blah.Frob()" kind of implies that Frob() will be executed asynchronously. It's true though that it doesn't imply that it will come back when Frob() is finished and continue where it left off.

    Maybe "async await" would have been a good choice but thats two words and I understand the reasons to try and keep it as only one.

  • If it doesn't introduce ambiguities, I'd be inclined to use 'async' for both operators.

  • @InBetween, if it was mistaken as "wait until this completes," the counter is it would naturally not be any different than if the keyword wasn't there at all! The program already does this waiting by default. But maybe you're right. Next time, use a word that don't mean nothing... like lupid.

  • How about await (because that does kind of imply "wait here until this finishes") change it to "return until". The idea being that the thread will return from this method, until this line finishes, and then it comes back.

  • I did'nt get my answer :(

    Re-posting my comment...

    Return statement in async method in reality is returning a value which is getting associated with Task and not something which looks like it is returning TO THE CALLER.

    async public Task<int> ReturnIntAsync()

    {

         return 0;

    }

    So the return statement can be modified something like

    async return OR task return, just to clarify what it is doing and also matches with the function signature.

    I have tried to explain this in my blog(my first blog:), please have a look.

    gauravsmathur.wordpress.com/.../something-wrong-with-async-await-and-the-tasktask

  • I suppose the reason this isn't sitting well with me is that with "async"/"await" is that you've chosen two words that, to someone not familiar what C# is really doing, imply the exact opposite of what's really happening.

    "Async" as a word already has a definition.  Something is happening outside of the current thread of execution.  Sticking that keyword on the *method* itself implies that the *method* will execute outside of the current thread of execution, which is the exact opposite of the truth.  That's not even to mention how difficult of a time someone unfamiliar with what the keyword does will have figuring it out when they go to their favorite search engine and search for "C# async".     I'd be much happier if the "async" keyword were renamed to "coroutine"; because then the keyword describes exactly what the method is, and coroutines aren't already part of C# culture in other, very different and incompatible ways, so someone looking for information about the new feature won't be lost in a sea of existing, contradictory information.

    "Await", too, is an unfortunate choice, since "wait" has a pretty solid definition already as well -- blocking a thread until something happens.  And again, what the keyword does is the opposite of that existing meaning.   "Yield until" seems to me the most appropriate choice from a terminology perspective, but "yield" already has a meaning in C#, and while it's similar, it's subtly different enough to not want to overload the term.   It's a shame the language team is set on using a single word for this, because there are plenty of expressive two-word phrases that would communicate the meaning clearly ("return until", "resume when", "continue after", etc.) without causing confusion with an existing concept; and I'm at a loss for a good single-word alternative that fills that void.

  • @Timothy, scratch "continue after" off. "continue" already has meaning. "continue after" could imply that you want to continue to the next loop iteration after the following command. On that note, "resume when" is too VB, but I'm not sure that's a valid enough reason to reject it outright.

  • I second Jon's suggestion. Using "async" for both operators makes the most sense to me, and it's very difficult to see how this could introduce ambiguities.

  • As others have stated its kind of hard to find the magical one word that conveys everyhting the async/await patter represents.

    I agree that maybe using words that have in most coders a predefined behavior (async) might not be the best idea but I'm not sure there is any good alternative out there.

    As a one word solution how would "resume" work instead of await?

  • I'd be tempted to make this feature a two-word, postfix operator 'when ready' such that I could say:

    var x = Frob(q) when ready;

  • @Anthony P

    int i=0;

    private int i=0;

    So now I must conclude that "private" must mean something different, after all its there for a reason right?

    Frankly if the C# team decided to use "frobAllDayLong" as the identifier instead of "await" it would still work, after all coders would end up learning what it means and use it correctly.

    The fact that the team is trying hard to come up with keywords that CONVEYS the meaning of the code

    makes me think that they consider it important, and to that degree IMHO "await" is not a perfect solution. Is it the best one word available? Probably yes, but its far from perfect as it can easily convey a completely different meaning.

  • The language designers have already invested a large amount of energy debating the pros and cons of various keywords for this new feature. They are happy with the keywords chosen. They've already released the community test preview. People are already using the chosen keywords. Others are already writing books about the new feature and using the chosen keywords.

    It doesn't matter that the ones chosen suck.

    Just before release, one of the designers is going to say to the others, "You know what? Those keywords suck! We should change them." At which point the rest of the design team is going to say, "We invested too much energy in making that decision to change it. We already released the CTP. There are already books being written. We can't change it now!"

  • @InBetween:

    The issue isn't knowing the concept once you've learned it.  It's the path to learning the feature in the first place if Mort or Elvis happen to stumble across this keyword they've never seen before in code they're maintaining.

    From that perspective, "frobAllDayLong" is more preferable than choosing a keyword which is already well established to mean something different, as is the case with "async".   Google returns 423,000 results in a search for "c# async", and every single one of those describes the inverse of what the "async" keyword actually does; whereas "c# frobAllDayLong" returns 0 results.   A developer trying to learn the meaning of the "async" keyword would need to dig through an ocean of highly misleading results describing a vastly different concept; and a developer trying to learn the meaning of the "frobAllDayLong" keyword only gets results relevant to what they're looking for.

    But at the same time I sadly believe @Tergiver above is correct.  The decision's already been made and is set in stone, for better or worse.

Page 1 of 3 (37 items) 123