Writing managed asynchronous code has been possible since the advent of IAsyncResult
in .NET 1.0. However, it has not been easy until async
/await
in .NET 4.5. Unfortunately, there are situations where you really need to implement asynchrony but are stuck with an older framework version. If you are on .NET 4.0 and want an easy way to use Task
-based async, then read on!
First, let’s talk about iterator blocks. Eric Lippert once referred to them as a “weak kind of coroutine.” Indeed, an iterator might more accurately be referred to as a generator, a special kind of coroutine. As you are probably aware, an iterator block allows you to generate a sequence of values one item at a time by yield
ing them to the caller:
// Infinite loop?? private static IEnumerable<string> Letters() { int i = 0; while (true) { char letter = (char)('A' + i); yield return letter.ToString(); if (++i == 26) { i = 0; } } } // Here we Take() only the first three, i.e. allow // the method to yield 3 times only and then move on. private static void PrintThreeLetters() { foreach (string letter in Letters().Take(3)) { Console.Write(letter); } Console.WriteLine(); }
How does it do this? Ultimately, the compiler generates a state machine class to hold the captured state (e.g. local variables in your method), keep track of the next step to execute, and so on. (Jon Skeet goes over some of the gory details in his C# in Depth article “Iterator block implementation details.”)
What does this have to with async? Quite a bit, actually! The use of iterators for asynchrony has a long history, going back at least as far as Jeffrey Richter‘s AsyncEnumerator
. Stephen Toub wrote about the possibilities of using iterators and Tasks together when .NET 4.5 was just a twinkle in Anders Hejlsberg‘s eye.
So just how can we use an iterator to simplify async? The basic idea is that we yield a result for every async step we need to invoke. A main loop on the outside keeps advancing the enumerator and executing these async calls. If the call completes immediately (i.e. synchronously), it handles the result and continues. If not, it schedules a continuation so that on completion, the iterator state machine will help us resume where we left off.
Complicated? Perhaps a little, for the implementer (me). But the job is made somewhat easier with the help of TDD and unit tests showing that all the behavior is designed and implemented correctly. And the end result makes for a much improved async experience for .NET 4.0 users.
So, without further ado, allow me to introduce AsyncOperation
, the iterator-based solution to Task-based async in .NET 4.0. The code is available in my AsyncEnumSample project on GitHub. The commit history shows how the code evolved via unit tests. I should note that this is one of the more complicated projects I have done for this blog with more than 35 unit tests, many of which initially passed without any code changes. This is because I wanted to be confident all behaviors were covered in case of later refactoring.
Here is the basic API surface area of AsyncOperation
:
namespace AsyncEnumSample { public abstract class AsyncOperation<TResult> { protected AsyncOperation(); protected interface IExceptionHandler { bool Handle(Exception exception); } protected TResult Result { get; set; } public Task<TResult> Start(); protected abstract IEnumerator<Step> Steps(); protected struct Step { public static Task<T> TaskFromResult<T>(T result); public static Task TaskFromException(Exception exception); public static Task<T> TaskFromException<T>(Exception exception); public static Step Await<TState>(TState state, Func<TState, AsyncCallback, object, IAsyncResult> begin, Action<TState, IAsyncResult> end, params IExceptionHandler[] handlers); public static Step Await<TState>(TState state, Func<TState, Task> doAsync, params IExceptionHandler[] handlers); public static Step Await<TState, TCallResult>(TState state, Func<TState, Task<TCallResult>> doAsync, Action<TState, TCallResult> afterCall, params IExceptionHandler[] handlers); public void Invoke(); } protected static class Catch<TException> where TException : Exception { public static IExceptionHandler AndHandle<TState>(TState state, Func<TState, TException, bool> handler); } } }
The Start
method drives the iterator loop which is implemented by a derived class via the Steps
method. Of particular importance is the Step
struct, which provides various Await
factory methods for different kinds of async steps. Three basic patterns are supported: old-style BeginXxx
/EndXxx
, Task
(no result), and Task<T>
(with result). (Note that a TState
object is expected to be provided to each delegate, to improve performance and prevent implicit capture of locals if desired.)
Sample usage for each type of call:
// Begin/End (assigning result to a member variable) yield return Step.Await( this, (thisPtr, c, s) => thisPtr.someObj.BeginOp(c, s), (thisPtr, r) => thisPtr.retValue = thisPtr.someObj.EndOp(r)); // Task (no result) yield return Step.Await( this, thisPtr => thisPtr.someObj.DoAsync()); // Task<T> (with result, assigning to member variable after completion) yield return Step.Await( this, thisPtr => thisPtr.someObj.DoWithResultAsync(), (thisPtr, r) => thisPtr.retValue = r);
Note that you must yield return
every Step
. It is unfortunately easy to forget this, which is one drawback of trying to build advanced patterns without real compiler support. Another limitation is that you can’t easily write catch blocks, however there is a relatively simple way around this using the provided Catch<TException>.AndHandle
functionality. An example:
protected override IEnumerator<Step> Steps() { // . . . yield return Step.Await( this, (thisPtr, c, s) => thisPtr.someObj.BeginOp(c, s), (thisPtr, r) => thisPtr.retValue = thisPtr.someObj.EndOp(r), Catch<SomeException>.AndHandle(this, (thisPtr, e) => thisPtr.Handle(e))); // . . . } private bool Handle(SomeException exception) { // . . . Do something with exception here . . . // If successfully handled ... return true; }
By default, any exception will transition the overarching Task
to a faulted state and rethrow to the caller. However, you can provide one or more handler methods that return a bool
indicating whether the exception should be handled (true
) or rethrown (false
).
Here is a full use case of an async file reader. Note the use of member variables to avoid implicit capture — in general, this should improve performance due to reduced code generation and heap allocation:
internal sealed class AsyncFileReader { private readonly int bufferSize; public AsyncFileReader(int bufferSize) { this.bufferSize = bufferSize; } public Task<byte[]> ReadAllBytesAsync(string path) { return new ReadAllBytesAsyncOperation(path, this.bufferSize).Start(); } private sealed class ReadAllBytesAsyncOperation : AsyncOperation<byte[]> { private readonly string path; private readonly int bufferSize; private FileStream stream; private byte[] buffer; private int bytesRead; public ReadAllBytesAsyncOperation(string path, int bufferSize) { this.path = path; this.bufferSize = bufferSize; } protected override IEnumerator<Step> Steps() { using (MemoryStream outputStream = new MemoryStream()) using (this.stream = new FileStream(this.path, FileMode.Open, FileAccess.Read, FileShare.Read, this.bufferSize, true)) { this.buffer = new byte[this.bufferSize]; do { Console.WriteLine("[{0}] Reading...", Thread.CurrentThread.ManagedThreadId); yield return Step.Await( this, (thisPtr, c, s) => thisPtr.stream.BeginRead(thisPtr.buffer, 0, thisPtr.bufferSize, c, s), (thisPtr, r) => thisPtr.bytesRead = thisPtr.stream.EndRead(r)); Console.WriteLine("[{0}] Read {1} bytes.", Thread.CurrentThread.ManagedThreadId, this.bytesRead); if (this.bytesRead > 0) { outputStream.Write(this.buffer, 0, this.bytesRead); } } while (this.bytesRead > 0); this.Result = outputStream.ToArray(); } } } }
Here is a snippet from the sample app included in the project, which reads a 1000000 byte file:
AsyncFileReader reader = new AsyncFileReader(100001); Task<byte[]> task = reader.ReadAllBytesAsync(path); byte[] readBytes = task.Result; Console.WriteLine("Read {0} bytes.", readBytes.Length);
Finally, the sample output:
[1] Reading... [5] Read 100001 bytes. [5] Reading... [5] Read 100001 bytes. [5] Reading... [5] Read 100001 bytes. [5] Reading... [5] Read 100001 bytes. [5] Reading... [5] Read 100001 bytes. [5] Reading... [5] Read 100001 bytes. [5] Reading... [7] Read 100001 bytes. [7] Reading... [7] Read 100001 bytes. [7] Reading... [7] Read 100001 bytes. [7] Reading... [7] Read 99991 bytes. [7] Reading... [7] Read 0 bytes. Read 1000000 bytes.
If you’re stuck with .NET 4.0, why not give AsyncOperation
a try and see how it eases the pain of async?