In the previous post I showed how to simulate async/await in .NET 4.0. This works great for Task-based async, but what about those who still use stone tools and .NET 3.5? Well, they can share in the fun, too! I have created AsyncEnum35Sample, a .NET 3.5-ized version of the .NET 4.0 AsyncEnumSample.
But first, a history lesson: long ago in .NET 3.5, before there was Task
, there was IAsyncResult
. Unlike Task
, IAsyncResult
is an interface which means you need to implement it yourself. You will find many examples on how to do this from various online sources. Perhaps the best starting point is Jeffrey Richter’s example in the MSDN article “Implementing the CLR Asynchronous Programming Model.”
Here are the basic mechanics of implementing an operation using the classic asynchronous programming model:
- Create a “Begin” method to initiate the operation. The method should have a return value of
IAsyncResult
and two input parameters,AsyncCallback
(callback) andobject
(state). If the method accepts additional input parameters, they should appear first, before the callback and state. - Create an “End” method to complete the operation. The method should have a single input parameter
IAsyncResult
. For a logically void operation, there should be no return value. Otherwise, the result of the operation should be returned. - If the operation completes synchronously with an error, it should throw on “Begin.” This is the only case when the callback is not invoked (except of course if no callback is provided, i.e. the user passes
null
). - If the operation completes asynchronously with an error, it should throw on “End.”
- If the operation succeeds and completes synchronously, the
CompletedSynchronously
bool should be set totrue
. This indicates that the callback is being invoked on the same thread that called “Begin.” - The “End” method should block until the operation is completed. If already completed, it should return immediately with the result (or throw on error).
It is tricky to get all this right; luckily the starting point provided by Jeffrey Richter helps with the details — my version with minor adjustments can be found here: AsyncResult<TResult>
.
As if it wasn’t hard enough for implementers… users of the asynchronous programming model must be mindful of the following:
- Always call “End.” Do this even if the method is logically void. The implementer is counting on you to do this as it may be the only way that the operation can be fully cleaned up. Note that the same instance of
IAsyncResult
returned from “Begin” or provided via the callback should be passed to the “End” method. - Prefer callbacks, never block. It is more efficient to call “End” in the async callback since it will never block there. (For one thing, this helps avoid creation of a
WaitHandle
to block until completion.) - Handle synchronous completion correctly. To avoid what Michael Marucheck refers to as a “stack dive” in his “Asynchronous Programming in Indigo” blog post, you should not blindly launch another asynchronous call from inside a completion callback, most notably if you are implementing a long running asynchronous loop. As shown in that blog post, you will want to check first if the result completed synchronously and then handle it outside the callback if so.
- Catch on “Begin” and “End.” Due to the possibility of synchronous completion, you must assume that the same set of exceptions can be thrown from both methods.
Whew. Legacy async is hard. Before you give up entirely, let’s take a look at what an asynchronous file reader looks like using AsyncOperation<TResult>
:
internal sealed class AsyncFileReader { private readonly int bufferSize; public AsyncFileReader(int bufferSize) { this.bufferSize = bufferSize; } public IAsyncResult BeginReadAllBytes(string path, AsyncCallback callback, object state) { return new ReadAllBytesAsyncOperation(path, this.bufferSize).Start(callback, state); } public byte[] EndReadAllBytes(IAsyncResult result) { return ReadAllBytesAsyncOperation.End(result); } 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(); } } } }
While it’s certainly non-trivial, this implementation of an async reader loop has to be at least 10 times easier than the code you’d have to write explicitly to achieve the same thing without AsyncOperation
. We have gained readability, well-defined exception “catch and transfer” behavior around all call sites, and proper handling of synchronous completion (no unnecessary blocking or thread switching). Not bad!
The inner details of the .NET 3.5 implementation of AsyncOperation
are not that different from the .NET 4.0 implementation. Instead of Task methods, we have the additional complexity of dealing with two call sites “Begin” and “End” and the IAsyncResult
handling. Otherwise, it’s just a basic enumerator which continues calling “Begin” and “End” until the first asynchronous completion. At that point, it returns control to the caller. On the next asynchronous callback, the enumeration begins again where it left off. If at any point an exception is thrown, it invokes the user’s catch handler(s). If not handled, the operation transitions to a “completed with exception” state and rethrows on “End” (except in the case of synchronous error completion, where the exception is rethrown directly).
If you find yourself faced with the daunting task of writing pre-.NET 4.0 async code, see if AsyncEnum35Sample can help.