Asynchrony in C# 5, Part One

Asynchrony in C# 5, Part One

Rate This
  • Comments 61

The designers of C# 2.0 realized that writing iterator logic was painful. So they added iterator blocks. That way the compiler could figure out how to build a state machine that could store the continuation - the “what comes next” - in state somewhere, hidden behind the scenes, so that you don’t have to write that code.

They also realized that writing little methods that make use of local variables was painful. So they added anonymous methods. That way the compiler could figure out how to hoist the locals to a closure class, so that you don't have to write that code.

The designers of C# 3.0 realized that writing code that sorts, filters, joins, groups and summarizes complex data sets was painful. So they added query comprehensions and all the rest of the LINQ features. That way the compiler could figure out how to do the right object model calls to build the query, the expression trees, and so on.

The designers of C# 4.0 realized that interoperating with modern and legacy dynamic object models was painful. So they added the dynamic type. That way the compiler could figure out how to generate the code at compile time that does the analysis in the Dynamic Language Runtime at runtime.

The designers of C# 5.0 realized that writing asynchronous code is painful, in so many ways. Asynchronous code is hard to reason about, and as we've seen, the transformation into a continuation is complex and leads to code replete with mechanisms that obscure the meaning of the code.

This shall not stand.

I am pleased to announce that there will be a C# 5.0 (*), and that in C# 5.0 you’ll be able to take this synchronous code:

void ArchiveDocuments(List<Url> urls)
{
  for(int i = 0; i < urls.Count; ++i)
    Archive(Fetch(urls[i]));
}

and, given reasonable implementations of the FetchAsync and ArchiveAsync methods, transform it into this code to achieve the goal of sharing wait times as described yesterday:

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);
  }
}

Where is the state machine code, the lambdas, the continuations, the checks to see if the task is already complete? They’re all still there. Let the compiler generate all that stuff for you, just like you let the compiler generate code for iterator blocks, closures, expression trees, query comprehensions and dynamic calls. The C# 5.0 code above is essentially a syntactic sugar for the code I presented yesterday. That's a pretty sweet sugar!

I want to be quite clear on this point: the action of the code above will be logically the same as the action of yesterday's code. Whenever a task is "awaited", the remainder of the current method is signed up as a continuation of the task, and then control immediately returns to the caller. When the task completes, the continuation is invoked and the method starts up where it was before.

If I’ve timed the posting of this article correctly then Anders is announcing this new language feature at the PDC right about… now. You can watch it here.

We are as of right now making a Community Technology Preview edition of the prototype C# 5.0 compiler available. The prototype compiler will be of prototype quality; expect features to be rough around the edges still. The idea is to give you a chance to try it and see what you think so that we can get early feedback.

I'll be posting more about this feature tomorrow and for quite some time after that; if you can't wait, or want to get your hands on the prototype, you can get lots more information and fresh tasty compiler bits from msdn.com/vstudio/async.

And of course, in keeping with our co-evolution strategy, there will be a Visual Basic 11 and it will also feature task-based asynchrony. Check out the VB team blog for details, or read all about this feature at my colleague Lucian's blog. (Lucian did much of the design and prototyping of this feature for both C# and VB; he, not I, is the expert on this so if you have deep questions, you might want to ask him, not me.)

Tomorrow: await? async? Task? what about AsyncThingy<T>? Tell me more!

(*) We are absolutely positively not announcing any dates or ship vehicles at this time, so don't even ask. Even if I knew, which I don't, and even if my knowledge had the faintest chance of being accurate, which it doesn't, I still wouldn't tell you.

  • As mentioned above: What about exception handling? Could I do something like this? (and what would the semantics be?)

    async void ArchiveDocuments(List<Url> urls)

    {

      try {

        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);

        }

      } catch (MyFetchException e)

      { ... }

    }

    Because ... without exception handling IO stuff is still going to be painfull (albeight a bit less than we are used to).

  • @Michael, yes, you can do exception handling of course. Watch Anders' presentation at PDC, he shows an example

  • Oh my God. You are completely hide the statemachine. Now everyone, who what to use this construct must know the statmachine behind, and must be able to simulate completelly the working of this statemachine in his/her brain. Now everyone who wants to use this construct must know all strange abstarction interference with other constructs, just for the minimal code lenght.

    Yes, this is very nice syntatic sugar for the CPS solution. But still not enough expressible.

    I would like to see something similar:

    async foreach(Url url in urls)

      { var document = FetchAsync(url); ArchiveAsync(await document); }

    This nicely express the dependency between archive and fetch and the independece of urls.

  • I have two questions:

    1) Where can the TaskCancelException occur? Only on an await statement, or anywhere inside the async method? I'm hoping only on await since anywhere would be almost as ugly as thread abortion.

    2) How does this work with functions that return their results piecewise?

    For example how would one write this synchronous code using the new async constructs?

    IEnumerable<string> DownloadStrings()

    {

    for(int i=0;i<100;i++)

      yield return DownloadString("abc.com/page"+i);

    }

    foreach(string s in DownloadStrings())

    {

     Display(s);

    }

  • @Szindbad: but the URLs aren't independent in his example; he wanted to run the next fetch operation concurrently to the archive operation, but not run multiple fetch operations concurrently.

    If parallelism between independent operations was the goal, C# 4.0 can already do it easily without using "async" at all:

    Parallel.ForEach(urls, url => Archive(Fetch(url));

    @W:

    1) AFAIK it can throw only when someone calls cancellationToken.ThrowIfCancellationRequested(). That means even "await" itself doesn't throw (but it can propagate exceptions thrown by the task it was waiting for).

    2) That's what I was complaining about: it simply doesn't work. async and yield are similar, yet can't be mixed. The example you gave CANNOT be written using the async construct unless you jump through some major hoops.

    Apparently the plan is to have the System.Reactive library jump through those hoops for you, but I'd prefer if C# allowed mixing of async and yield.

  • The last week was like the week before Christmas. And today Santa delivered his presents.

  • I it possible to "await" an event?

  • Re: "The designers of". I do not like that. Always thought they had realized a bit more than you mention, in all cases.

    Re: "I am pleased to announce that there will be a C# 5.0". I am very pleased to here that.

    Re: "transform it into this code". Nice sugar, but the archive variable is not intuitive. If it is set to null and not used anywhere, how can one expect it is not null? I guess it is already too late to move or use it, but still...

    I would also add more sugar so that created async tasks can be killed easily. What if they are processor hungry and I have lost interest in the result?

  • Re-read the article and understood how archive can be not null. Great sugar!

  • Wow - just like tasks and rendezvous points that Ada had in the 1980's

  • That is so ugly.  Look at Google Go's approach instead.

  • I know you're not announcing dates, but you included one more bit of information that Anders and others didn't - you mentioned "C# 5". I was starting to have some hope that this feature might ship as an official "go live" update, but I'd imagine that C# 5 would be further out and would include more changes.

    This change simplifies Silverlight development so much and seems to work well, so it'd be a shame not to be able to use it for a long time because we're waiting for the rest of C# 5.

  • Try Nemerle. It's much better than  this

  • I think yield task would make it clear that a Task is coming from the statement.

  • Looks a lot like F# asynchronous workflows :)

Page 3 of 5 (61 items) 12345