Search code examples
asynchronousf#cancellation

Cooperative cancellation in F# with cancel continuation


Probably I have here 2 questions instead of one, but anyway.

I'm implementing cooperative cancellation as here suggested. Here is my test code:

type Async with
    static member Isolate(f : CancellationToken -> Async<'T>) : Async<'T> =
        async {
            let! ct = Async.CancellationToken
            let isolatedTask = Async.StartAsTask(f ct)
            return! Async.AwaitTask isolatedTask
        }

let testLoop (ct: CancellationToken) = async {
    let rec next ix =
        if ct.IsCancellationRequested then ()
        else 
            printf "%i.." ix
            Thread.Sleep 10  
            next (ix+1)
    next 1
}

let cancellationSource = new CancellationTokenSource()
let onDone () = printfn "!! DONE"
let onError _ = printfn "!! ERROR"
let onCancel _ = printfn "!! CANCEL"

Async.StartWithContinuations (Async.Isolate testLoop, onDone, onError, onCancel, cancellationSource.Token)

Thread.Sleep(100)
cancellationSource.Cancel ()
Thread.Sleep(500)

As you can see, I start async with done, cancel and error continuations. If I run that code as is, I'll get the following output:

1..2..3..4..5..6..7..8..!! DONE

If I slightly update the Isolate method as follows:

    static member Isolate(f : CancellationToken -> Async<'T>) : Async<'T> =
        async {
            let! ct = Async.CancellationToken
            let isolatedTask = Async.StartAsTask(f ct)
            let! x = Async.AwaitTask isolatedTask
            x
        }

I get the expected (by myself) output:

1..2..3..4..5..6..7..!! CANCEL

Why do we have such difference in the behavior?

Is it possible to abort the testLoop, if it does not cancelled within some timeout?


Solution

  • The async block checks for cancellation of the Async.CancellationToken only before and after bind (written using let!). This means that when the token gets cancelled, the workflow will only get cancelled when there is more work to be done.

    It is also worth noting that isolatedTask does not itself gets cancelled in this example, because it just terminates regularly (using if).

    In your case:

    • When you use just return!, the task returns regularly, Async.AwaitTask returns regularly and nothing is done afterwards so the workflow completes.

    • When you use let! followed by return, the task returns regularly and Async.AwaitTask returns regularly, but then let! checks for cancellation before running return and this cancels the workflow.