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
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.
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.
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.
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.
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.
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.
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
- Converting asynchronous cancellation from C# to F# | Tyson Williams
- Asynchronous Programming in F# | Packt
- Async programming in F# | Microsoft Docs
- control.fs | GitHub
- The F# Asynchronous Programming Model
Top comments (1)
Thanks! Just the clear overview and information on the various ways of starting an async that I needed.