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 AwaitTask
does not return null
, but instead, the task throws OperationCanceledException
exception. So, you do not get back None
value, but instead, you get an exception (and then F# also runs your finally
block).
The confusing thing is that cancellation is a special kind of exception that cannot be handled in user code inside the async
block - once your computation is cancelled, it cannot be un-cancelled and it will always stop (you can do cleanup in finally
). 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.CancellationToken
which 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With