Cancellation Tokens in F#
Tags: beginners, programming, dotnet, fsharp
Published: 2022-02-17 11:18:45

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.

```fsharp
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
```

```
Cancellation token main: 59231349
Starting
Cancellation token: 59231349
Waiting
Waiting
Cancelled
```

## 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.

```fsharp
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
```

```
Cancellation token main: 3139257
Starting
Cancellation token: 3139257
Starting Child
Cancellation token child: 3139257
Waiting Child
Waiting Child
Cancelled Task Child
Cancelled
```

#### 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.

```fsharp
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
```

```
Cancellation token main: 52169655
Starting
Cancellation token: 52169655
Starting Child
Cancellation token child: 6634750
Waiting Child
Waiting Child
Cancelled Task Child
Cancelled
```

#### 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.

```fsharp
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
```

```
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
```

#### 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.

```fsharp
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
```

```
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
```

#### 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.

```fsharp
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
```

```
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
```

#### 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.

```fsharp
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
```

```
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
```

### 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

- [Asynchronous programming | F# for Fun and Profit](https://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/#cancelling-workflows)
- [Converting asynchronous cancellation from C# to F# | Tyson Williams](https://tysonwilliams.coding.blog/2020-12-01_csharp_task_to_fsharp_async)
- [Asynchronous Programming in F# | Packt](https://hub.packtpub.com/asynchronous-programming-f/)
- [Async programming in F# | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/async)
- [control.fs | GitHub](https://github.com/fsharp/fsharp/blob/577d06b9ec7192a6adafefd09ade0ed10b13897d/src/fsharp/FSharp.Core/control.fs)
- [The F# Asynchronous Programming Model](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/async-padl-revised-v2.pdf)

#### Co-Author 

{% embed https://dev.to/piotrjustyna %}