F# async: event-based asynchronous pattern
The event-based asynchronous pattern is a model for asynchronous programming in .NET in which an operation reports its completion through an event. Here is a simple example:
private static void AsyncDownloadGoogle()
{
var uri = new Uri("http://google.com");
var client = new WebClient();
DownloadDataCompletedEventHandler completed = null;
completed = (_, e) =>
{
// handle the operation's result
if (e.Cancelled)
{
Console.WriteLine("Operation cancelled.");
}
else if (e.Error != null)
{
Console.WriteLine("Error: {0}", e.Error);
}
else
{
byte[] page = e.Result;
Console.WriteLine("Downloaded {0} bytes", page.Length);
}
// unsubscribe from the event
client.DownloadDataCompleted -= completed;
};
// subscribe to the completion event
client.DownloadDataCompleted += completed;
// start the asynchronous operation
client.DownloadDataAsync(uri);
// allow the user to cancel the operation
Console.WriteLine("Press [esc] to cancel");
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Escape)
{
client.CancelAsync();
}
}
The operation’s result is exposed through event arguments derived from System.ComponentMode.AsyncCompletedEventArgs. These arguments also indicate whether the operation was cancelled or failed. Two details require some care:
- If the same object will be reused for another asynchronous operation with a different result handler, the previous handler should be removed once the operation completes. This takes a little extra bookkeeping when the handler is an anonymous method or lambda expression, as in the example above.
- Some classes support multiple concurrent operations on the same instance, though
System.Net.WebClientdoes not. To distinguish those operations, the methods used to start and cancel them need overloads that accept an additionalobject userStateparameter. Completion handlers must then check theUserStateproperty of the event arguments.
How can we use this pattern in F# without all the manual event handling? F# async workflows let us express asynchronous operations almost as concisely as synchronous ones.
To use an operation within an async workflow, however, we need a method that wraps it in an Async<'a> value, representing an asynchronous computation. The F# standard library provides several such extension methods for common .NET classes. It also provides Async.FromBeginEnd(), which creates an Async<'a> from a pair of Begin and End methods following the Asynchronous Programming Model (APM).
The event-based asynchronous pattern has no equivalent wrapper in the standard library at the time of writing. The following two extension methods bridge that gap, adapting event-based operations to F#’s Async<'a> type. They also illustrate how to use Async.FromContinuations:
module AsyncExtensions
open System
open System.ComponentModel
#nowarn "40"
type Async with
/// Convert an operation with a start method and a completion
/// event (the event-based asynchronous pattern) into an
/// F# asynchronous computation.
static member FromEventPattern
(completedEvent : IObservable<_>, // completion event
executeAction : unit -> unit, // start the operation
?cancelAction : unit -> unit) = // cancel the operation
// start the operation with the supplied continuations
let comp (onValue, onError, onCancel) =
let onCancel () =
onCancel (OperationCanceledException())
// subscribe to the operation's completion event
let rec subscription : IDisposable =
completedEvent.Subscribe {
new IObserver<#AsyncCompletedEventArgs> with
// check the operation's status when the event fires
member x.OnNext(args) =
use __ = subscription // unsubscribe on exit
if args.Cancelled then onCancel ()
elif args.Error = null then onValue args
else onError args.Error
// ordinary events do not call this, but an arbitrary
// IObservable<_> may, so handle it as well
member x.OnError(exc) =
use __ = subscription in onError exc
member x.OnCompleted() =
use __ = subscription in onCancel ()
}
try executeAction () // start the asynchronous operation
with _ ->
use __ = subscription // if starting fails,
reraise () // unsubscribe immediately
// create the asynchronous computation
let operation = Async.FromContinuations comp
match cancelAction with // if a cancellation action was supplied,
| Some action -> // register it with Async.OnCancel
async { use! __ = Async.OnCancel action
return! operation }
| None -> operation
/// Convert an operation with a start method and a completion
/// event (the event-based asynchronous pattern) into an
/// F# asynchronous computation, with support for multiple
/// concurrent operations.
static member FromEventPattern
(completedEvent : IObservable<_>, // completion event
executeAction : obj -> unit, // start the operation
?cancelAction : obj -> unit, // cancel the operation
?userToken : obj) = // operation identifier
// create an identifier if none was supplied
let token = match userToken with Some token -> token
| None -> new obj()
// pass the identifier to the cancellation action, if any
let cancel = Option.map (fun f () -> f token) cancelAction
Async.FromEventPattern<#AsyncCompletedEventArgs>(
completedEvent = // filter completion events
Observable.filter // by operation identifier
(fun e -> e.UserState = token) completedEvent,
?cancelAction = cancel,
executeAction = fun() -> executeAction token)
We can now define an extension member for WebClient’s DownloadData operation. Note the Async prefix, following the F# naming convention for these methods:
type WebClient with
member this.AsyncDownloadData(uri: Uri) =
Async.FromEventPattern(
this.DownloadDataCompleted,
(fun()-> this.DownloadDataAsync uri),
(fun()-> this.CancelAsync()))
We can also transform the result so that the workflow returns the downloaded data rather than the event arguments:
type WebClient with
member this.AsyncDownloadData(uri: Uri) =
async {
let! e = Async.FromEventPattern(
this.DownloadDataCompleted,
(fun()-> this.DownloadDataAsync uri),
(fun()-> this.CancelAsync()))
return e.Result
}
Using this extension, we can express the original example in F# as follows:
open System
open System.Net
open System.Threading
let asyncDownloadGoogle() =
let uri = Uri("http://google.com")
let client = new WebClient()
use token = new CancellationTokenSource()
let work = async {
// handle cancellation of the async workflow
use! cancel = Async.OnCancel (fun() ->
printfn "Operation cancelled.")
// run the asynchronous operation and process its result
try let! page = client.AsyncDownloadData(uri)
printfn "Downloaded %d bytes" page.Length
// handle asynchronous errors
with e -> printfn "Error: %s" e.Message
}
Async.RunSynchronously(work, cancellationToken = token.Token)
let key = Console.ReadKey true
if (key.Key = ConsoleKey.Escape) then token.Cancel()
Using CancellationTokenSource still requires some setup, including a cancellation handler inside the workflow. In return, it provides a general cancellation mechanism, while F# propagates the token through the workflow automatically. There is no need to pass it explicitly through every asynchronous call.