C# 5 promises to bring asynchrony into the language (Eric Lippert’s series is a great introduction to the subject, and this post assumes some familiarity with it). I think it’s a great step forward, and it would make asynchronous programming a lot easier.
We already know how to think in Tasks instead of threads, .NET 4.0 taught us that.
We already know how to use continuations (or at least some weak form of it), C# 2.0 iterators taught us that.
So, as an experiment, I went ahead and implemented a weak form of the await/async “magic” using “the materials in the room”, i.e. tasks and iterators - minus the syntactic sugar, of course. Let’s have a look.
First, a sample
In C# 5 we would have (taken from Anders’ Netflix sample):
async void LoadMoviesAsync(int year)
{
while (true)
{
var movies = await QueryMoviesAsync(year, imageCount, pageSize, cts.Token);
if (movies.Length == 0) break;
DisplayMovies(movies);
}
}
While in my implementation it would look like this:
IEnumerable<Task> LoadMovies(int year)
{
while (true)
{
var moviesTask = Async.Await<Movie[]>(result =>
QueryMovies(result, year, imageCount, pageSize, cts.Token));
yield return moviesTask;
var movies = moviesTask.Result;
if (movies.Length == 0) break;
DisplayMovies(movies);
}
}
What’s going on here?
First of all, as you can see, the methods look very similar. We have created an iterator that allows us to start running the method and break after every yield. The implementation of Async.Await() method is surprisingly simple:
- The Await() method simply creates an enumerator, which starts up the state machine.
- It calls MoveNext(), which executes the method up to the next yield.
- We get a Task from the yield, and attach a continuation to it, which calls MoveNext() again, and so on.
public static Task<T> Await<T>(Func<TaskCompletionSource<T>, IEnumerable<Task>> func)
{
var completionSource = new TaskCompletionSource<T>();
Await(func(completionSource));
return completionSource.Task;
}
public static void Await(IEnumerable<Task> iterator)
{
IEnumerator<Task> enumerator = iterator.GetEnumerator();
Run(enumerator);
}
private static void Run(IEnumerator<Task> enumerator)
{
if (enumerator.MoveNext())
{
enumerator.Current.ContinueWith(t => Run(enumerator));
}
}
Why do we need TaskCompletionSource?
The return value of the iterator method must always be IEnumerable<Task>. But what if we want to return a value from a method? Remember, it doesn’t execute synchronously anymore! That is why methods that (originally) do not return void, have the option of adding an argument of TaskCompletionSource. In the method, we can call TaskCompletionSource.SetResult() to set the result. We also return the Task the TCS creates, so the caller could access the result. The overload of Await() that we use in this case simply wraps around this functionality, and enables a more concise syntax.
When an asynchronous method has no return value, TaskCompletionSource is not needed.
What’s missing?
This implementation doesn’t deal with task schedulers (making sure we’re in the right context for UI operations), and its exception handling is limited.
Update: C# 5 has since shipped with async and await built into the language, so this is now only of historical interest.