13

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?

vtortola
  • 34,709
  • 29
  • 161
  • 263

1 Answers1

16

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.

Community
  • 1
  • 1
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • About your first point, maybe I did not explain myself correctly. `AcceptWebSocketAsync` is a method declared inside a C# library that accepts a `CancellationToken`. When the cancellation is triggered, that method returns `null` rather than a `WebSocket`. This is specifically designed this way. – vtortola Nov 19 '14 at 16:57
  • 1
    @vtortola I clarified how F# handles this - but basically, once the token is cancelled, F# async will not let you continue running the async workflow (it cannot be un-cancelled). So, if you really want to continue the workflow, I'd create a separate `CancellationTokenSource` just for the method (and not pass it to `Async.Start`) – Tomas Petricek Nov 19 '14 at 16:59
  • I see, thanks for that. I am still wrapping my head around this. – vtortola Nov 19 '14 at 17:02
  • 1
    I got a bit confused too when writing the answer :-). But I think the key point (*once workflow is cancelled, it cannot be uncancelled*) makes good sense. So you need more advanced management of cancellation tokens. – Tomas Petricek Nov 19 '14 at 17:04
  • Actually, if I create a custom token and pass it to `Async.Start` it happens exactly the same that with `Asyn.DefaultCancellationToken`. I was told there would be nice parallel programming, but so far I don't feel very comfortable about what I do in F#, I hope it is a matter of time :) Cheers. – vtortola Nov 19 '14 at 17:18
  • 1
    Well, yes, if you create a custom token and pass it to `Async.Start` then the workflow gets cancelled (when the token is cancelled) and only `finally` blocks will run during the cleanup. So you'd need a separate token for that one method (and then do not pass it to `Async.Start` if you do not want to cancel the workflow). – Tomas Petricek Nov 19 '14 at 17:21
  • `final` block? is that a typo? did you mean `finally` block? – knocte Aug 09 '18 at 10:29
  • @knocte Thanks, fixed! – Tomas Petricek Aug 09 '18 at 10:39