For the next part of my coverage of Asynchronous Computation Expressions, I'm going to talk about the things you get for free when you use them. I was reminded of this during a recent chat with Greg Young about how hard it was during asynchronous operations to notify the controlling thread of an exception. These asynchronous operations are meant to handle such things.
Let's get caught up to where we are today with regards to Async Computation Expressions:
- Task Parallel Library and Async Computation Expressions
- Adding Async Operations to Asynchronous Computation Expressions
Brief Introduction to Asynchronous Computation Expressions
Asynchronous operations, which use the Async<'a> type, are basically a way of writing Continuation Passing Style (CPS) programs. There is a reason I covered that in a previous post here about CPS in regards to Recursion, which is why it's important to learn these concepts. These asynchronous computations are computations that call a success continuation should the operation succeed and an exception continuation should it fail. Together, this provides a managed computation expression where you get a few things for "free".
Listed below are some of the things you get for "free" from the managed computation expressions. Each of these will be covered in detail in this post:
- Exception Propagation
- Cancellation Checking
- Resource Lifetime Management
Let's go into each in detail as it makes the story around the asynchronous computation expressions quite intriguing. I'll hold off on a lot of the details as I'll go into that further in a subsequent post.
Resource Management
Simply put, when you use the "use" keyword inside of the asynchronous computation expression, at the end of the scope, the resource will be disposed. Even should an exception occur, this still happens. As you may recall, F# has ways of handling resources through the use of the using function, or the use keyword.
using(File.OpenRead("file.txt")) (fun stream -> ())
let main() =
use stream = File.OpenRead("file.txt")
() // Resource closed here
Either I can use the using keyword which then takes a function which takes a single input and returns the result where the input must implement System.IDisposable. The use keyword is syntactic sugar over the same way of doing this. Now that we understand this, let's move onto using this inside of an asynchronous computation expression. As you may note, we can write our computation using the same style.
async {
use stream = File.OpenRead(file)
use reader = new StreamReader(stream)
let text = reader.ReadToEnd()
return text
}
Async.Run (read_file "file.txt")
As you may notice, our resources will be cleaned up just as it hits the return statement and the stream and reader are no longer needed. Should an exception occur, the resources still will be cleaned up. I'll cover that in the next section, what exactly happens there. But, we also have the ability to bind to asynchronous operations by using the "use!" and "let!" keyword such as the following:
let read_file file =
async {
use! stream = File.OpenReadAsync(file)
use reader = new StreamReader(stream)
let! text = reader.ReadToEndAsync()
return text
}
Async.Run (read_file "file.txt")
Now that we understand the basics, let's move onto exception management and propagation.
Exception Propagation
One of the harder topics to deal with during asynchronous operations is exception management. The asynchronous computation expressions add this behavior for free. If an exception is thrown during an asynchronous step, the exception will then terminate the entire computation and then clean up any resources that were allocated by using the "use" keyword. The exception is then propagated via the exception continuation back to the controlling thread. You can also handle these exceptions by using a try/catch/finally block in F# such as this:
async {
let! stream = File.OpenReadAsync(file)
try
use reader = new StreamReader(stream)
let! text = reader.ReadToEndAsync()
return text
finally
stream.Close()
}
But, what happens when you get a failure? Let's take this code snippet for example:
async {
do if n % 2 <> 0 then failwith "Not even"
return n * n
}
let result = Async.Run (square_even 3)
What's going to happen is that we get a FailureException thrown such as the following:
But what about when we run multiple computations in parallel? Tasks runnning the Async.Run will report any failure back to the controlling thread. When you run them in parallel, it is non-deterministic which one will fail first. Only the first failure will be reported, and an attempt will be made to cancel the rest of the computations. Any further failures are ignored.
async {
do if n % 2 <> 0 then failwith proc
return n * n * n
}
let failing_tasks = [ (task_even 3 "1"); (task_even 5 "2")]
Async.Run(Async.Parallel failing_tasks)
And the result will look like this:
If you notice, only the first exception was noted. Had I written this slightly different, the second task could have thrown an exception first and would be noted in the same way.
But, what if you don't want this exception thrown, but instead handed back to you to deal with in your own way? By using the Async.Catch combinator, you get a choice whether you get the value of the computation or the exception. Below is an example of using that concept:
let square_even n =
async {
do if n % 2 <> 0 then failwith "Not even"
return n * n
}
let result = Async.Run (Async.Catch (square_even 3))
(* val result : Choice<int, exn> *)
let print_value (c:Choice<int, exn>) =
match c with
| Choice2_1 a -> print_any a
| Choice2_2 b -> print_any b.Message
print_value result
What I've been able to do is catch the exception should it be thrown. The result is given to me as a choice of either an exception or the real value. In order to extract the value, I need to go through pattern matching, as the Choice<'a, 'b> is actually a discriminated union. Depending on the input, this program could print either the result, or "Not even".
Cancellation Checking
Cancellation checking behavior in asynchronous computation expressions is quite similar to Thread.Abort in terms of semantics. What that means is the following:
- Cancellation is non-deterministic about whether it will succeed
- The item that caused the cancellation will be notified whether the cancellation succeeded or not.
- The finally blocks will all be executed.
- Cancellation may not be caught, but instead will be thrown as an OperationCancelledException should it have been run through the Async.Run.
Cancellation checks are handled for us in the following ways:
- At each "let!" "do!" or "use!" bind operation and is checked for the cancellation flag
- Manually, it can be checked by calling "do! Async.CancelCheck()"
- If the cancellation flag is set, then the cancellation continuation is called
Below is a simple call with a check for cancellation:
async {
use! stream = File.OpenReadAsync(file)
use reader = new StreamReader(stream)
let! text = reader.ReadToEndAsync()
do! Async.CancelCheck()
return text
}
let run_async =
try
Async.Run (read_file "file.txt")
with
| :? System.OperationCanceledException -> string.Empty
With this, we could try to cancel the operation using the Async.CancelDefaultGroup method or through an AsyncGroup had you created your function through that.
Wrapping It Up
I hope this opened your eyes to the possibilities with asynchronous computation expressions in F#. They are very interesting and intriguing parts of F#. Without deep down knowledge of continuations and monads, you can still write very powerful programs in an asynchronous manner. These are exciting times as Brian McNamara (F# Team Member) has pointed out, we are getting closer and closer to the CTP of F#. Download the latest bits for F# and try it out.
I'm heading off to enjoy some R&R time for a much needed vacation, so this blog will be quiet for a little bit.
Source Click Here.
No comments:
Post a Comment
Post your comments here: