DEV Community

André Silva
André Silva

Posted on • Edited on

Cancellation Tokens in F#

This post targets the Async library. In the newer versions of F# (dotnet 6.0+), the Task library was introduced. In this article I'm going to focus only in the Async library.

Basic usage of Cancellation Tokens in F

To illustrate how cancellation tokens are used, we can isolate two scenarios: asynchronous functions chained together where cancellation tokens are propagated through the execution chain and asynchronous functions executed on their own, without further, chained asynchronous calls. Let us focus on the second scenario first.

By specifying the timeout in the constructor of CancellationTokenSource we are requesting the task to be cancelled after 200 milliseconds. In this example, we make a simple console print to show the task has been cancelled.

Please note in this example, the cancellation token is not explicitly passed to the Async.Sleep function, that's why we don't need to check if the token has been cancelled in the following examples.

open System.Threading

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        while (true) do
            printfn "Waiting"

            do! Async.Sleep(100)
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 59231349
Starting
Cancellation token: 59231349
Waiting
Waiting
Cancelled
Enter fullscreen mode Exit fullscreen mode

Complex usage of Cancellation Tokens in F

For this scenario, we want to show how a cancellation token is shared from the parent task to the child tasks by using the Async module functions to start new tasks.

In this section, we will describe and explain cancellation tokens in functions such as Async.StartChild or Async.Start.

Code Examples

In this section, it will be added a few code examples with selected Async functions that help us execute/start asynchronous expressions.

We are using the Async.OnCancel function to show to the user the task was properly cancelled and as stated in the previous section are not doing any checks if we should cancel the tasks since it's done by the function Async.Sleep. This happens simply because the F# runtime does that job for us and does all the cancellation it needs, only in cases where we share the cancellation tokens with the child tasks.

do! / let! / return! / use!

This is the easiest way to start a new task in F#. In this example, the do! is making sure the Cancellation Token is shared with all child tasks, so it means that when there is a cancellation of the token, the tasks should be cancelled, this is proved by the hash code that is printed in the output.

open System.Threading

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            printfn "Waiting Child"

            do! Async.Sleep(100)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        do! taskChild()
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 3139257
Starting
Cancellation token: 3139257
Starting Child
Cancellation token child: 3139257
Waiting Child
Waiting Child
Cancelled Task Child
Cancelled
Enter fullscreen mode Exit fullscreen mode

Async.StartChild

This starts a child computation within an asynchronous workflow. This allows multiple asynchronous computations to be executed simultaneously.

In this example, the cancellation token is not shared, but internally when a task is started as a child, the child cancellation token is linked to the parent cancellation token. That's the reason why we have different hash codes for the parent and child tasks but both get cancelled.

open System.Threading

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            printfn "Waiting Child"

            do! Async.Sleep(100)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        let! completor = taskChild () |> Async.StartChild
        let! result = completor

        return result
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 52169655
Starting
Cancellation token: 52169655
Starting Child
Cancellation token child: 6634750
Waiting Child
Waiting Child
Cancelled Task Child
Cancelled
Enter fullscreen mode Exit fullscreen mode

Async.Start

Starts the asynchronous computation in the thread pool. In this example, the cancellation token is not being propagated to the child task since we are using the Start function to trigger the task initialization. This way of kicking off a task might be useful for work where we want a fire and forget behaviour since the child task is never cancelled.

This function accepts a cancellation token as well by argument. When a token is shared, the child task is cancelled.

open System.Threading

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            printfn "Waiting Child"

            do! Async.Sleep(100)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        Async.Start(taskChild ())

        do! Async.Sleep(1000)
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 24709875
Starting
Cancellation token: 24709875
Starting Child
Cancellation token child: 40392858
Waiting Child
Waiting Child
Cancelled
Waiting Child
Waiting Child
Waiting Child
Enter fullscreen mode Exit fullscreen mode

Async.StartImmediate

Runs an asynchronous computation, starting immediately on the current operating system thread. This is very similar to the Async.Start function we saw earlier. The same rules regarding cancellation tokens apply to this function as well.

open System.Threading

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            printfn "Waiting Child"

            do! Async.Sleep(100)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        Async.StartImmediate(taskChild ())

        do! Async.Sleep(1000)
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 59545942
Starting
Cancellation token: 59545942
Starting Child
Cancellation token child: 40392858
Waiting Child
Waiting Child
Cancelled
Waiting Child
Waiting Child
Waiting Child
Enter fullscreen mode Exit fullscreen mode

Async.RunSynchronously

In this function, it gets a task by argument and runs it synchronously. It also exposes a cancellation token that is passed as an argument, it will be used as the cancellation token for the child tasks.

open System.Threading

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            printfn "Waiting Child"

            do! Async.Sleep(100)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        taskChild () |> Async.RunSynchronously

        printfn "Line after RunSynchronously"
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 12025920
Starting
Cancellation token: 12025920
Starting Child
Cancellation token child: 40392858
Waiting Child
Waiting Child
Cancelled
Waiting Child
Waiting Child
Waiting Child
Enter fullscreen mode Exit fullscreen mode

Working with multiple sub-tasks

Interestingly, the tasks respect the order that they have been called to be cancelled. In this example, the Grand Child task is the first to be cancelled, whereas the first one called was the last one to be cancelled. As expected, the Grand Child 2 task is never called.

Also please note that task 2 is never called since task 1 never finishes the execution.

open System.Threading

let taskGrandchild(order: int) : Async<unit> =
    async {
        printfn $"Starting Grandchild({order})"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token GrandChild({order}): {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn $"Cancelled Task Grandchild: {order}")

        while (true) do
            printfn $"Waiting Grandchild({order})"

            do! Async.Sleep(100)
    }

let taskChild() : Async<unit> =
    async {
        printfn "Starting Child"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token child: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled Task Child")

        while (true) do
            do! taskGrandchild(1)
            do! taskGrandchild(2)
    }

let task () : Async<unit> =
    async {
        printfn "Starting"

        let! ct = Async.CancellationToken
        printfn $"Cancellation token: {ct.GetHashCode()}"

        use! c = Async.OnCancel(fun () -> printfn "Cancelled")

        do! taskChild()
    }

let cts = new CancellationTokenSource(200)
let ct = cts.Token
printfn $"Cancellation token main: {ct.GetHashCode()}"
Async.Start(task (), cts.Token)
Async.Sleep 500 |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode
Cancellation token main: 26465690
Starting
Cancellation token: 26465690
Starting Child
Cancellation token child: 26465690
Starting Grandchild(1)
Cancellation token GrandChild(1): 26465690
Waiting Grandchild(1)
Waiting Grandchild(1)
Cancelled Task Grandchild: 1
Cancelled Task Child
Cancelled
Enter fullscreen mode Exit fullscreen mode

Comparison Table from the Code Examples

In the table below we are comparing the different calls and behaviours.

Async Function Shares CT Accepts CT by Parameter? Creates Linked CTS Good Application
do! / let! Yes No No a/b/e
Async.StartChild No No Yes a/b/e
Async.Start No Yes No c/d
Async.StartImmediate No Yes No c/d
Async.RunSynchronously No Yes No c/d

Examples:

a) Compute a value asynchronously

b) Save transactions in the database

c) Send an event in an Event-Driven system

d) Create a log in the logging system

e) Retrieve or post information into an API that results from what we need later

References

Co-Author

Top comments (1)

Collapse
 
realparadyne profile image
David

Thanks! Just the clear overview and information on the various ways of starting an async that I needed.