Introduction
So, you've read about the C#5 async and await keywords and how they help simplify asynchronous programming. Alas, your employer (or you) upgraded to Visual Studio 2010 just two years ago and isn't ready to shell out another round for VS 2012. You are stuck with VS 2010 and C#4 where that feature is unsupported. (This article also applies to VB.NET 2010; different syntax, but same approach.) You're left pining, "Oh, how much clearer my code would be if only I could write methods in VS 2010 which look synchronous but perform asynchronously."
After reading this article, you will be able to do just that. We will develop a small piece of infrastructure code which does the heavy lifting, allowing us to write synchronous-looking asynchronous methods (SLAMs) in a manner like that enjoyed in C#5. (Note: If you are already using C#5, of if you're satisfied using Microsoft's unsupported Async CTP, then this article does not apply to you.)
We must admit at the start that async/await is a fine topping of syntactic sugar which we don't have, so our code will be a little more salty than it would be with them. But it will be far more palatable than the bitter taste of writing our own IAsyncResult callbacks! And when you finally upgrade to VS 2012 (or beyond), it will be a trivial matter to convert your methods to take advantage of the C#5 keywords; it will require simple syntactic changes, not a laborious structural rewrite.
Overview
The async/await keywords are built on the Task Asynchronous Pattern. TAP is well-documented elsewhere, so I won't cover it here. I must add as a personal note: TAP is super cool! You can create lots of little units of work (tasks) to be completed at some time; tasks can start other (nested) tasks and/or set up continuation tasks to begin only when one or more antecedent tasks have completed. A task doesn't necessarily tie up a thread (a heavyweight resource) while nested tasks complete. And you don't have to worry about scheduling threads to execute tasks; this is handled automatically by the framework with minimal helpful hints from you. Then when you set your program running, all the tasks trickle down to completion, bouncing off each other like steel balls in a virtual Pachinko Machine!
In C#4 we don't have async and await, but we do have the Task types minus only a few .NET 5 addenda which we can do without or build ourselves.
In a C#5 async method, you would await a Task. This does not cause the thread to wait; instead, the method returns a Task to its caller, on which it can await (if itself async) or attach continuations. (It could also Wait() on the task or its Result, but this will tie up the thread, so avoid that.) When the awaited task completes successfully, your async method continues where it left off.
As you may know, the C#5 compiler rewrites its async methods into a generated nested class which implements a state machine. There is another feature of C# (since 2.0) which does exactly that: iterators (with yield return). The idea here is to use an iterator method to build the state machine in C#4, returning a sequence of Tasks which are the steps to await in the overall process. We will develop a method which accepts an enumeration of tasks returned by the iterator, returning a single overriding Task which represents the completion of the entire sequence and provides its final Result (if any).
The End Goal
Stephen Covey advised us to begin with the end in mind. That's what we'll do here. Examples abound of how to write SLAMs with async/await. How will we write them without those keywords? Let's start with a simple C#5 async method and see how to represent it in C#4. Then we'll discuss more generally how to transform any code segments which need it.
Here is how we might write Stream.CopyToAsync() in C#5 using asynchronous reads and writes, if it weren't already available in .NET 5. (We can use the transformed version since .NET 4 doesn't have it! Download the sample code for ReadAsync() and WriteAsync().)
public static async Task CopyToAsync(
this Stream input, Stream output,
CancellationToken cancellationToken = default(CancellationToken))
{
byte[] buffer = new byte[0x1000]; while (true) {
cancellationToken.ThrowIfCancellationRequested();
int bytesRead = await input.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
cancellationToken.ThrowIfCancellationRequested();
await output.WriteAsync(buffer, 0, bytesRead);
}
} For C#4, we'll break this into two: one same-accessibility method and one private method with identical arguments but different return types. The private method is the iterator implementing the same process, resulting in a sequence of tasks (IEnumerable<Task>) to await. The actual tasks in the sequence can be non-generic or generic of varying types, in any combination. (Fortunately, generic Task<T> types are subtypes of the non-generic Task type.)
The same-accessibility (here "public") method returns the same type as the corresponding async method would: void, Task, or a generic Task<T>. It is a simple one-liner which invokes the private iterator and transforms it into a Task or Task<T> using an extension method.
public static Task CopyToAsync(
this Stream input, Stream output,
CancellationToken cancellationToken = default(CancellationToken))
{
return CopyToAsyncTasks(input, output, cancellationToken).ToTask();
}
private static IEnumerable<Task> CopyToAsyncTasks(
Stream input, Stream output,
CancellationToken cancellationToken)
{
byte[] buffer = new byte[0x1000]; while (true) {
cancellationToken.ThrowIfCancellationRequested();
var bytesReadTask = input.ReadAsync(buffer, 0, buffer.Length);
yield return bytesReadTask;
if (bytesReadTask.Result == 0) break;
cancellationToken.ThrowIfCancellationRequested();
yield return output.WriteAsync(buffer, 0, bytesReadTask.Result);
}
} The asynchronous method name usually ends with "Async" (unless it's an event handler, e.g. startButton_Click). Give its iterator the same name appending "Tasks" (e.g. startButton_ClickTasks). If the asynchronous method returns void, it still calls ToTask() but doesn't return the Task. If the asynchronous method returns a Task<X>, then it invokes a generic ToTask<X>() extension method. For the three return types, the async-replacement method looks as follows:
public void DoSomethingAsync() {
DoSomethingAsyncTasks().ToTask();
}
public Task DoSomethingAsync() {
return DoSomethingAsyncTasks().ToTask();
}
public Task<String> DoSomethingAsync() {
return DoSomethingAsyncTasks().ToTask<String>();
} The paired iterator method isn't much more complicated. Where the async method would await a non-generic Task, the iterator simply yields it. Where the async method would await a task result, the iterator saves the task in a variable, yields it, then uses its Result afterward. Both cases are shown in the CopyToAsyncTasks() example above.
For a SLAM with a generic result Task<X>, the iterator must yield a final task of that exact type. ToTask<X>() will typecast the final task to that type to extract its Result. Often your iterator will calculate the value from intermediate task results and just needs to wrap it in a Task<T>. .NET 5 provides a convenient static method for this. We don't have it in .NET 4, so we will implement it as TaskEx.FromResult<T>(value).
The last thing you need to know is how to handle a return from the middle. An async method can return from an arbitrarily nested block; our iterator mimics this by ending the iteration after yielding the return value (if any).
public async Task<String> DoSomethingAsync() {
while (…) {
foreach (…) {
return "Result";
}
}
}
private IEnumerable<Task> DoSomethingAsyncTasks() {
while (…) {
foreach (…) {
yield return TaskEx.FromResult("Result");
yield break;
}
}
} Now we know how to write a SLAM in C#4, but we can't actually do it until we implement FromResult<T>() and two ToTask() extension methods. Let's get to it.
An Easy Start
We will implement our 3 methods in a class System.Threading.Tasks.TaskEx, starting with the two which are straightforward. FromResult<T>() creates a TaskCompletionSource<T>, populates its result, and returns its Task.
public static Task<TResult> FromResult<TResult>(TResult resultValue) {
var completionSource = new TaskCompletionSource<TResult>();
completionSource.SetResult(resultValue);
return completionSource.Task;
} Clearly, the two ToTask() methods are essentially identical, the only difference is in whether the returned task has a result value. We don't want to code and maintain the same process twice, so we will implement one using the other. The generic implementation will look for a "marker type" to know that we don't really care about a result value, and it will avoid typecasting the final task. Then we can implement the non-generic version using the marker type.
private abstract class VoidResult { }
public static Task ToTask(this IEnumerable<Task> tasks) {
return ToTask<VoidResult>(tasks);
} So far, so good. Now all that's left is to implement the generic ToTask<T>(). Hang on, guys, we goin' for a ride.
A Naïve First Attempt
For our first attempt at implementing the method, we'll enumerate the returned tasks, Wait() for each to complete, then set the result from the final task (if appropriate). Of course, we don't want to tie up the current thread during this process, so we'll start another task to perform this loop.
public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks)
{
var tcs = new TaskCompletionSource<TResult>();
Task.Factory.StartNew(() => {
Task last = null;
try {
foreach (var task in tasks) {
last = task;
task.Wait();
}
tcs.SetResult(
last == null || typeof(TResult) == typeof(VoidResult)
? default(TResult) : ((Task<TResult>) last).Result);
} catch (AggregateException aggrEx) {
if (aggrEx.InnerExceptions.Count != 1) tcs.SetException(aggrEx);
else if (aggrEx.InnerException is OperationCanceledException) tcs.SetCanceled();
else tcs.SetException(aggrEx.InnerException);
} catch (OperationCanceledException cancEx) {
tcs.SetCanceled();
} catch (Exception ex) {
tcs.SetException(ex);
}
});
return tcs.Task;
} There are some good things here, and it actually works as long as it doesn't touch a User Interface:
- It correctly returns a
TaskCompletionSource's Task and sets completion status via the Source. - It shows how we can set the task's final
Result using the iterator's last task, avoiding that when no result is desired. - It catches exceptions from the iterator to set
Canceled or Faulted status. It also propagates the enumerated task's status (here via Wait() which may throw an AggregateException wrapping the cancellation or fault exception).
But there are major problems here. The most egregious are:
- For the iterator to live up to its "synchronous-looking" promise, then when it's initiated from a UI thread, the iterator method should be able to access UI controls. You can see here that the
foreach loop (which calls into the iterator) runs in the background; don't touch the UI from there! This approach does not respect the SynchronizationContext. - We have problems outside of a UI, too. We may want to spawn many, many Tasks in parallel implemented by a SLAM. But look at that
Wait() inside the loop! While waiting on a nested task, possibly a long time for a remote operation to complete, we are tying up a thread. We would starve ourselves of thread pool threads. - Unwrapping the
AggregateException this way is just plain hokey. We need to capture and propagate its completion status without it throwing an exception. - Sometimes the SLAM can determine its completion status immediately. In that case, a C#5
async method would operate synchronously and efficiently. We always schedule a background task here, so we've lost that possibility.
It's time to get creative!
Looping by Continuation
The big idea is to obtain the first task yielded from the iterator immediately and synchronously. We set up a continuation so that when it completes, the continuation checks the task's status and (if it was successful) obtains the next task and sets up another continuation; and so on until finished. (If ever it does, that is; there's no requirement that an iterator ends.)
public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks)
{
var taskScheduler =
SynchronizationContext.Current == null
? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext();
var tcs = new TaskCompletionSource<TResult>();
var taskEnumerator = tasks.GetEnumerator();
if (!taskEnumerator.MoveNext()) {
tcs.SetResult(default(TResult));
return tcs.Task;
}
taskEnumerator.Current.ContinueWith(
t => ToTaskDoOneStep(taskEnumerator, taskScheduler, tcs, t),
taskScheduler);
return tcs.Task;
}
private static void ToTaskDoOneStep<TResult>(
IEnumerator<Task> taskEnumerator, TaskScheduler taskScheduler,
TaskCompletionSource<TResult> tcs, Task completedTask)
{
var status = completedTask.Status;
if (status == TaskStatus.Canceled) {
tcs.SetCanceled();
} else if (status == TaskStatus.Faulted) {
tcs.SetException(completedTask.Exception);
} else if (!taskEnumerator.MoveNext()) {
tcs.SetResult(
typeof(TResult) == typeof(VoidResult)
? default(TResult) : ((Task<TResult>) completedTask).Result);
} else {
taskEnumerator.Current.ContinueWith(
t => ToTaskDoOneStep(taskEnumerator, taskScheduler, tcs, t),
taskScheduler);
}
} There is a lot to appreciate here:
- Our continuations use a
TaskScheduler which respects the SynchronizationContext, if there is one. This allows our iterator, invoked immediately or from a continuation, to access UI controls when initiated from the UI thread. - The process runs by continuations, so no threads are tied up waiting! Incidentially, that call within
ToTaskDoOneStep() to itself is not a recursive call; it's in a lambda which is invoked after the taskEnumerator.Current task completes. The current activation exits almost immediately after calling ContinueWith(), and it does so independently of the continuation. - We check each nested task's status directly within its continuation, not by inspecting an exception.
- The first iteration occurs synchronously.
However, there is at least one huge problem here and some lesser ones.
- If the iterator throws an unhandled exception, or cancels by throwing an
OperationCanceledException, we don't handle it and set the master task's status. This is something we had previously but lost in this version. - To fix problem #1, we would have to introduce identical exception handlers in both methods where we call
MoveNext(). Even as it is now, we have identical continuations set up in both methods. We are violating the "Don't Repeat Yourself" rule. - What if Async method's task is expected to provide a
Result, but our iterator exits without providing one? Or what if its final task is of the wrong type? In the first case, we silently return the default result type; in the second, we throw an unhandled InvalidCastException and the master task never reaches a completion state! Our process would hang indefinitely. - Finally, what if a nested task cancels or faults? We set the master task status and never invoke the iterator again. It may have been inside a
using block or try block with a finally and have some cleaning up to do. We should Dispose() the iterator when it terminates, not wait for the garbage collector to do it. How do we do that? With a continuation, of course!
To fix these issues, we'll remove the MoveNext() call from ToTask() and instead make an initial synchronous call to ToTaskDoOneStep(). Then we can add appropriate exception handling in one place.
The Final Version
Here is the final implementation of ToTask<T>(). It returns a master task using a TaskCompletionSource, never causes a thread to wait, respects the SynchronizationContext if any, handles exceptions from the iterator, propagates nested task completion directly (without AggregateException), returns a value to the master task when appropriate, faults with a helpful exception when the SLAM iterator doesn't end with a valid result, and disposes the enumerator when it completes.
public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks) {
var taskScheduler =
SynchronizationContext.Current == null
? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext();
var taskEnumerator = tasks.GetEnumerator();
var completionSource = new TaskCompletionSource<TResult>();
completionSource.Task.ContinueWith(t => taskEnumerator.Dispose(), taskScheduler);
ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, null);
return completionSource.Task;
}
private static void ToTaskDoOneStep<TResult>(
IEnumerator<Task> taskEnumerator, TaskScheduler taskScheduler,
TaskCompletionSource<TResult> completionSource, Task completedTask)
{
TaskStatus status;
if (completedTask == null) {
} else if ((status = completedTask.Status) == TaskStatus.Canceled) {
completionSource.SetCanceled();
return;
} else if (status == TaskStatus.Faulted) {
completionSource.SetException(completedTask.Exception);
return;
}
Boolean haveMore;
try {
haveMore = taskEnumerator.MoveNext();
} catch (OperationCanceledException cancExc) {
completionSource.SetCanceled();
return;
} catch (Exception exc) {
completionSource.SetException(exc);
return;
}
if (!haveMore) {
if (typeof(TResult) == typeof(VoidResult)) { completionSource.SetResult(default(TResult));
} else if (!(completedTask is Task<TResult>)) { completionSource.SetException(new InvalidOperationException(
"Asynchronous iterator " + taskEnumerator +
" requires a final result task of type " + typeof(Task<TResult>).FullName +
(completedTask == null ? ", but none was provided." :
"; the actual task type was " + completedTask.GetType().FullName)));
} else {
completionSource.SetResult(((Task<TResult>) completedTask).Result);
}
} else {
taskEnumerator.Current.ContinueWith(
nextTask => ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, nextTask),
taskScheduler);
}
} Voila! Now you can write SLAMs (synchronous-looking asynchronous methods) in Visual Studio 2010 with C#4 (or VB.NET 2010), where async and await are not supported.
About the Download Sample
The downloaded project contains two infrastructure files which you can compile into your assembly to support asynchronous programming in .NET 4: TaskExtensions.cs has the methods developed in this article; AsyncIoExtensions.cs provides some methods added in .NET 5 to support asynchronous stream and web operations. (It is surely a simple matter to translate them for use in VB.NET 2010.)
As examples, MainWindow.xaml.cs implements two asynchronous methods as described in this article, and it makes productive use of a continuation in an event handler. The sample is derived from an Async/Await Walkthrough project. For an exercise, remove the ToTask() methods and try re-implementing the asynchronous methods only with task continuations or other callbacks. If the process is linear and all waits are at the top level, the code is ugly but not too difficult to write. As soon as the needed await falls within a nested block, it becomes virtually impossible to keep the same semantics and remain asynchronous (i.e. to never Wait() on a Task) without using the method described in this article.
Differences from Async/Await
I was not able to get the Async CTP to install into Visual Studio, so I didn't try different scenarios with await. Therefore I must admit that I made an assumption about how it works which turned out not to be true. The technique described here is still better than not having it in C#4, but you need to know its limitations. (I finally wrote up a test program and compiled it the "old school" way: with text editor and csc.)
So far I have found just one. C#5 does not assume that you want to propagate the exception or cancellation status from an awaited task to the master task. Instead, it throws the nested task's exception or a TaskCanceledException from the point where you awaited it. You may then catch and handle the exception or let it propagate. This is not an option with yield return because an iterator cannot yield a value from within a try block with an exception handler. Lacking try-catch here, the best choice therefore is what the framework already does: to propagate the status.
Points of Interest
Up until the final versions, I was passing a CancellationToken into ToTask() and propagating it into the ToTaskDoOneStep() continuations. (It was irrelevant noise for this article, so I removed them. You can see commented-out traces in the sample code.) This was for two reasons. First, when handling OperationCanceledException I would check its CancellationToken to be sure it matched the one for this operation. If not, it would be a fault instead of a cancellation. While technically correct, it's so unlikely that cancellation tokens would get mixed up that it wasn't worth the trouble of passing it into the ToTask() call and between continuations. (If you Task experts can give me a good use case in the comments where this might validly happen, I'll reconsider.)
The second reason was that I could check if the token was canceled before each MoveNext() call into the iterator, cancel the master task immediately, and exit the process. This provides cancellation behavior without your iterator having to check the token. I wasn't convinced it was the right thing to do (since cancellation at some given yield return may be inappropriate for an asynchronous process) — better perhaps that's it's fully under the iterator process control — but I wanted to try it. It didn't work. I found that in some cases, the task would cancel and its continuations would not fire. In the sample code I'm depending on a continuation to re-enable the buttons, but it wasn't happening reliably, so sometimes the buttons were left disabled after the process was canceled. I show the cancellation check in the sample code, commented out; you can put the cancellation token method parameters back in and try it out. (If any Task experts can explain why this problem occurs, I'll appreciate it!)
History
2012-12-06
2012-12-11
- Added "Differences from Async/Await" section
Keith started programming in 8th grade in BASIC, then in Z80 (TRS-80) and 6502 (Apple II) assembly. In high school he disassembled Apple DOS and Basic to see how they worked and wrote a program to teach chess openings. At the University of Texas at Austin, Keith completed a newly-created Honors Program for entering CS students, then taught Pascal to other students before graduating in 1988 with a Bachelor of Science in Computer Science, with only three other students receiving the same degree. (Prior to that year, UT had offered only a BA for CS students.) He completed formal education with a Master of Computer Science degree from the University of Virginia.
Keith has worked with many different companies, often on a contract/consulting basis, in C++, Smalltalk, Java, and C#, developing applications for the desktop, client server, enterprise integration, and finally for the web. He currently enjoys his job at AirWatch building integration and web components for Mobile Device Management.
Passionate about software development, Keith's main non-work interests are in language design, compiler construction, and framework development. He also enjoys chess and jogging. Keith lives in Atlanta, GA with his wife and young son.