This week, at PDC 2010, Anders Hejlsberg introduced what is to become C# 5.0 and it’s major new feature: Making Async easy. The main facility for achieving this goal is the language keyword pair or async
and await
. Together they allow synchronous methods to be called in an asynchronous way without having to jump through all sorts of hoops.
This is very exciting stuff for us here at MindTouch, since we’ve built almost exactly that facility with our Asynchronous Method Pattern and Coroutine infrastructure in DReAM. But the one thing that has always bothered us what the additional manual code and sometimes unintuitive syntax required.
The Result and Task based asynchronous patterns
Steve Bjorg introduced Result<T>
with DReAM on top of the .NET 2.0 and mono 1.1.16 back in 2006. You can find a quick overview of it here. I've also covered it extensively in my monoconf talk. It's is similar in purpose to the Task
class in the Task Parallel Library introduced by Microsoft in .NET 4.0. Both of them are a kind of future, a handle for a value that may or may not have been produced yet. Both provide asynchrony patterns for easily calling a method that promises to produce a result eventually.
The MindTouch Asynchronous Method Pattern (AMP) takes this form:
Result<T> MethodName(...input paramaters..., Result<T> result)
where the trailing Result<T>
is used to set up conditions on the return value, such as Timeout.
The Task-based Asynchronous Pattern (TAP) looks like this:
Task<T> MethodNameAsync(...input paramaters...)
Both are easy enough to call in blocking form, but that defeats their purpose. Both could also be set up with continuation handles, which get executed once a value T
is available, but that takes the return code out of the current flow. And as completions call other asynchronous methods, your code just wanders off the right-hand side of the screen. This is common problem with continuations and is both a aesthetic and readability problem, in that your logically linear (if non-blocking) execution flow becomes increasingly difficult to follow.
Simplifying async flow with MindTouch Coroutines
What would be really nice is being able to call an asynchronous method and write the code that deals with the result on the next line, while simultaneously having your code suspend itself, freeing up your thread while the asynchronous code completes.
One way to approach this is with coroutines, i.e. a method that can exit and resume multiple times, rather having only one entry point. A coroutine can yield control to the async method and resume after it completes. In .NET 2.0 the yield
operator was added to C# for the iterator pattern. If you turn the iterator pattern upside down, you end up with a method that yields not iteration results but continuations to some engine executing the control flow, i.e. MindTouch Coroutines:
Result<XDoc> r; yield return r = Download(uri, new Result<XDoc>); var doc = r.Value;
This provided us with the ability to write methods in synchronous style that could call to potentially long running asynchronous processes but continue their linear flow without ever blocking the thread. For a more detailed look at how coroutines are implemented check this article. However, the cost of achieving this came with some syntactic artifacts.
As illustrated above, we can't declare and assign the result type T in the yield return, hence the pre-declaration of Result<XDoc> r
and the trailing var doc = r.Value
. Three lines instead of one. In C# 3.0, using Lambdas, we reduced this to two lines:
XDoc doc; yield return doc = DownloadAndProcess(uri, new Result<XDoc>).Set(v => doc = v);
Another artifact is that a coroutine has to return an enumerator. We use IYield
, which Result
implements and coroutines require the return type IEnumerator<IYield>
and by convention use a Result<T>
as their last argument. That means coroutines can yield AMPs but are not AMPs themselves. As I stated above, a coroutine is an iterator turned upside down, yielding control to some executing engine. This means that every AMP that uses a coroutine can be written using this convention:
Result<XDoc> DownloadAndProcess(XUri uri, Result<XDoc> result) { return Coroutine.Invoke(DownloadAndProcess_Coroutine, uri, result); } IEnumerator<IYield> DownloadAndProcess_Coroutine(XUri uri, Result<XDoc> result) { XDoc doc; yield return Plug.New(uri).Get(new Result<DreamMessage>) .Set(v => doc = v.ToDocument()); // do some more async work result.Return(doc); }
Note: I snuck in Plug
here, which is our fluent interface wrapper around HttpWebRequest
and implements the AMP, internally using the async interface of HttpWebRequest
, i.e. we can yield a Plug.Get
and it will resume execution once the request completes.
C# 5.0 async/wait continuation flow
With C# vNext, Microsoft introduces two new language keywords that basically let you create the same flow we provide via the AMP and Coroutines, but using Task
and leaving the syntactic cost to the compiler instead. This means that the TAP can be used without having to define a custom method with a different signature to execute the async work flow:
async Task<XDoc> DownloadAndProcess(XUri uri) { var doc = await Plug.New(uri).Get(); // do some more async work return doc; }
The async
keyword marks the method as one that the compiler needs to rewrite a sequence of continuations. The await
keyword tells the compiler that a TAP method is being called and to squirrel the rest of the method body into a continuation that is executed once the Task<XDoc>
has a result. Here the compiler takes on the work of letting you use a single method signature to call either as a regular method or as source of a continuation and rewrites a TAP call to yield execution flow and resume after completion. Using async/await
certainly reduces ceremony and makes it all more readable. We wish we could have had that luxury when we embarked on this pattern 4 years ago. Note: The above code pre-supposes a version of Plug
that implements the TAP instead of the AMP.
yield vs. await vs. something else
Shortly after Anders' PDC talk Eric Lippert posed the question whether await
is the best name for this new facility in the post Asynchronous Programming in C# 5.0 part two: Whence await? on his blog. The comment thread is a good read of arguments for and against various keywords. Personally being partial to yield
, I read Jon Skeet's case against it in his post C# 5 async and choosing terminology: why I'm against "yield" with interest. His point is that "[w]hen the action completes very quickly and synchronously, it isn't yielding at all." My personal feeling is that whether or not the call returns very quickly is an implementation detail invisible to the caller. The caller is yielding control to the callee, who makes the decision whether to return immediately or suspend the caller until completion.
However await
does seem like the least appropriate of the options, since we're really not waiting at all. Syntax like continue after
, continue with
and continue when
certainly seem more appropriate and descriptive of the action taking place. While yield return
perfectly describes to me what happens, maybe that is a sign the terminology reveals the implementation detail rather than the intent. The caller doesn't care about the yielding of control. The caller just wants the result from the call and get on with its business. So from a clarity of code perspective, I have to side with the continue (after|with|when)
camp.
When can I use async/await?
The CTP is available now. Of course, there is no release date, taking a gaming industry "when it's done!" stance. My bet is that async/await
will be usable in the next mono release (2.10? 3.0?) before C# 5.0 ships. Any way you look at it, though, it's going to be a while before you can rely on it existing on a platform you didn't personally configure.
In the meantime, DReAM coroutines are battle tested, powering all REST services in the MindTouch product and any other product using DReAM, and you can be sure that as C#5 approaches we will be tracking its progress closely to evaluate using the TAP in DReAM. We certainly would love to take advantage of the simplified syntax, while staying compatible with our existing syntax.