I was pretty comfortable with how async cancellations where done in C# with the TPL, but I am a little bit confused in F#. Apparently by calling Async.CancelDefaultToken() is enough to cancel outgoing Async<'T> operations. But they are not cancelled as I expected, they just... vanishes... I cannot detect properly the cancellation and tear down the stack properly.
For example, I have this code that depends on a C# library that uses TPL:
type WebSocketListener with
member x.AsyncAcceptWebSocket = async {
let! client = Async.AwaitTask <| x.AcceptWebSocketAsync Async.DefaultCancellationToken
if(not(isNull client)) then
return Some client
else
return None
}
let rec AsyncAcceptClients(listener : WebSocketListener) =
async {
let! result = listener.AsyncAcceptWebSocket
match result with
| None -> printf "Stop accepting clients.\n"
| Some client ->
Async.Start <| AsyncAcceptMessages client
do! AsyncAcceptClients listener
}
When the CancellationToken passed to x.AcceptWebSocketAsync is cancelled, returns null, and then AsyncAcceptWebSocket method returns None. I can verify this with a breakpoint.
But, AsyncAcceptClients (the caller), never gets that None value, the method just ends, and "Stop accepting clients.\n" is never displayed on the console. If I wrap everything in a try\finally :
let rec AsyncAcceptClients(listener : WebSocketListener) =
async {
try
let! result = listener.AsyncAcceptWebSocket
match result with
| None -> printf "Stop accepting clients.\n"
| Some client ->
Async.Start <| AsyncAcceptMessages client
do! AsyncAcceptClients listener
finally
printf "This message is actually printed"
}
Then what I put in the finally gets executed when listener.AsyncAcceptWebSocket returns None, but the code I have in the match still doesn't. (Actually, it prints the message on the finally block once for each connected client, so maybe I should move to an iterative approach?)
However, if I use a custom CancellationToken rather than Async.DefaultCancellationToken, everything works as expected, and the "Stop accepting clients.\n" message is print on screen.
What is going on here?
There are two things about the question:
First, when a cancellation happens in F#, the
AwaitTaskdoes not returnnull, but instead, the task throwsOperationCanceledExceptionexception. So, you do not get backNonevalue, but instead, you get an exception (and then F# also runs yourfinallyblock).The confusing thing is that cancellation is a special kind of exception that cannot be handled in user code inside the
asyncblock - once your computation is cancelled, it cannot be un-cancelled and it will always stop (you can do cleanup infinally). You can workaround this (see this SO answer) but it might cause unexpected things.Second, I would not use default cancellation token - that's shared by all async workflows and so it might do unexpected things. You can instead use
Async.CancellationTokenwhich gives you access to a current cancellation token (which F# automatically propagates for you - so you do not have to pass it around by hand as you do in C#).EDIT: Clarified how F# async handles cancellation exceptions.