The Asynchronous Programming Model (APM) in the .NET Framework has been around since .NET 1.0 and is the most common pattern for asynchrony in the Framework. Even if you’re not familiar with the name, you’re likely familiar with the core of the pattern. For a given synchronous operation Xyz, the asynchronous version manifests as BeginXyz and EndXyz: BeginXyz starts the operation and the EndXyz method joins with it (completes it). There are several mechanisms by which the results can be joined with, such as by polling for completion using the IsCompleted property on the IAsyncResult returned from BeginXyz, blocking until the operation has completed by waiting on the IAsyncResult’s AsyncWaitHandle, simply calling EndXyz and passing it the IAsyncResult (which will block until the operation has completed), or passing to BeginXyz a callback which will be invoked when the operation has completed: that callback should then call EndXyz to retrieve the results.
This pattern is so common that we’ve opted to incorporate it as a first-class citizen into the Task Parallel Library. One way we’ve done this is by having the Task class itself implement IAsyncResult: this means that Task can be used as the core of a Begin/End implementation, easing the implementation for common scenarios. One feature new to .NET 4 Beta 1 since previous CTPs, however, is that we now support the inverse, easy creation of tasks from an implementation of the APM pattern. This new functionality shows up through the FromAsync methods on TaskFactory and TaskFactory<TResult>.
Under the covers, FromAsync utilizes the TaskCompletionSource<TResult> type. Let’s say you wanted to create a Task that represents an asynchronous read on a Stream. You could write something like the following:
public static Task<int> ReadTask(this Stream stream,
byte [] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch(Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
You could of course write a similar wrapper for every Begin/End pair you wanted to utilize, but that could get tedious and error-prone. To solve that, we’ve provided the FromAsync method, which takes advantage of the common structure of the APM pattern, along with delegate inference, to provide generic overloads that work with most APM implementations.
For example, let’s say I have a Stream and a byte buffer, and I want to read from that stream into the buffer. Synchronously, I could do something like:
int bytesRead = stream.Read(buffer, 0, buffer.Length);
Creating a Task that does the same thing asynchronously:
Task<int> bytesRead = Task<int>.Factory.FromAsync(
stream.BeginRead, stream.EndRead, buffer, 0, buffer.Length, null);
Under the covers, we follow a pattern very similar to the one shown earlier as a specific implementation for Stream.Read. Combine this support with the ability to do Task.WaitAll, Task.WaitAny, Task.Factory.ContinueWhenAll, and Task.Factory.ContinueWhenAny, and you can achieve some very useful coordination functionality in very little code.
Of course, we don’t have any magic at our disposal; all of this functionality is available through standard .NET libraries. That means that we achieve the above with a method with the following signature:
public Task<TResult> FromAsync<TArg1, TArg2, TArg3>(
Func<TArg1, TArg2, TArg3, AsyncCallback,
object, IAsyncResult> beginMethod,
Func<IAsyncResult, TResult> endMethod,
TArg1 arg1, TArg2 arg2, TArg3 arg3,
object state);
You’ll notice then that this overload is coded specifically for APM implementations that take three input parameters (of types TArg1, TArg2, and TArg3). The vast majority of the APM implementations in the .NET Framework take three or less input parameters, with only a trickling accepting more than that. As such, we’ve included overloads in the Task Parallel Library that follow this pattern for up to and including three parameters. Of course, as with all corner cases, there will certainly be scenarios that require usage with more than three parameters, or with slightly different forms. For those cases, we’ve also added overloads that accept an IAsyncResult as the first parameter, rather than accepting a beginMethod delegate. This way, you can pass in any IAsyncResult you want, and we’ll call back to the endMethod when we find that the IAsyncResult has completed. This approach typically isn’t as efficient as the beginMethod approach, but it can be quite handy in a pinch.
0 comments