Search code examples
asynchronousf#cancellationcontinuations

Using the cancellation continuation in FromContinuations


I'm trying to understand async workflows via an Async<'T> I create with Async.FromContinuations, and can't see how to use the cancellation continuation. I'm trying this:

open System

let asyncComputation divisor =
    Async.FromContinuations
        (fun (success, except, cancel) ->
            try
                printfn "Going to sleep..."
                Threading.Thread.Sleep 3000
                printfn "...waking up"
                1 / divisor |> ignore
                printfn "Calling success continuation..."
                success ()
            with
            | :? OperationCanceledException as e ->
                printfn "Calling cancellation continuation..."
                cancel e
            | e ->
                printfn "Calling exception continuation..."
                except e)

[<EntryPoint>]
let main argv =
    use tokenSource = new Threading.CancellationTokenSource ()
    Async.Start (asyncComputation (int argv.[0]), tokenSource.Token)
    Console.ReadLine () |> ignore
    tokenSource.Cancel ()

Running with argument 1 causes the success continuation to be called after waking; and running with argument 0 causes the exception continuation to be called after waking, yielding the expected exception output. So far so good. But when I cancel (with either argument) by hitting the Enter key during the 3-second sleep, it apparently cancels the async computation without calling the cancellation continuation. So how should the cancellation continuation be used in FromContinuations, and how should the cancellation be triggered so that it invokes the cancellation continuation?


Solution

  • If you use CancellationToken in an asynchronous computation, F# async workflows will automatically propagate it, so that you can access it anywhere, but you still need to explicitly check if cancellation has been triggered and throw an exception yourself. This does not happen "automagically" anywhere in your synchronous user code.

    This means that there are two parts to the question. 1) How to access the cancellation token and 2) how to check if it has been cancelled.

    The second part is a bit tricky, becuase you need to use Async.CancellationToken, but this returns Async<CancellationToken> and so you have to call it in an async block, outside of the FromContinuations code. The second part is just a matter of calling tok.ThrowIfCancellationRequested() somewhere in your code.

    The following checks for cancellation just before performing division:

    let asyncComputation divisor = async {
        let! tok = Async.CancellationToken
        return! Async.FromContinuations
            (fun (success, except, cancel) ->
                try
                    printfn "Going to sleep..."
                    Threading.Thread.Sleep 3000
                    printfn "...waking up"
                    tok.ThrowIfCancellationRequested()
                    1 / divisor |> ignore
                    printfn "Calling success continuation..."
                    success ()
                with
                | :? OperationCanceledException as e ->
                    printfn "Calling cancellation continuation..."
                    cancel e
                | e ->
                    printfn "Calling exception continuation..."
                    except e) }
    

    This does not cancel while waiting. If you wanted that, you'd need to be able to cancel the waiting operation, for example using something like:

    Async.RunSynchronously(Async.Sleep 3000, cancellationToken = tok)
    

    But I suppose waiting here is just for question purposes and you are actually doing some other (possibly cancellable, possibly not) operation.