F# How Async<'T> cancellation works?
Asked Answered
M

1

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?

Melli answered 19/11, 2014 at 16:8 Comment(0)
S
17

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.

Stortz answered 19/11, 2014 at 16:46 Comment(8)
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.Melli
@Melli 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)Stortz
I see, thanks for that. I am still wrapping my head around this.Melli
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.Stortz
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.Melli
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).Stortz
final block? is that a typo? did you mean finally block?Vharat
@Vharat Thanks, fixed!Stortz

© 2022 - 2024 — McMap. All rights reserved.