Asynchronous workflow

Purpose

  1. Do not block the current thread while waiting for a long calculation

  2. Allow parallel calculations

  3. Indicate that a calculation may take some time

Async<'T> type

Represents an asynchronous calculation

πŸ“† Similar to the async/await pattern, way before Cβ™― and JS

  • 2007: Async<'T> Fβ™―

  • 2012: Task<T> .NET and pattern async/await

  • 2017: Promise JavaScript and pattern async/await

Methods returning an Async object

Async.AwaitTask(task : Task or Task<'T>) : Async<'T> β†’ Convert a Task (.NET) to Async (Fβ™―)

Async.Sleep(milliseconds or TimeSpan) : Async<unit> ≃ await Task.Delay() β‰  Thread.Sleep β†’ does not block current thread

FSharp.Control CommonExtensions module: extends the System.IO.Stream type (doc) β†’ AsyncRead(buffer: byte[], ?offset: int, ?count: int) : Async<int> β†’ AsyncWrite(buffer: byte[], ?offset: int, ?count: int) : Async<unit>

FSharp.Control WebExtensions module: extends type System.Net.WebClient (doc) β†’ AsyncDownloadData(address : Uri) : Async<byte[]> β†’ AsyncDownloadString(address : Uri) : Async<string>

Run an async calculation

Async.RunSynchronously(calc: Async<'T>, ?timeoutMs: int, ?cancellationToken) : 'T β†’ Waits for the calculation to end, blocking the calling thread! (β‰  await Cβ™―) ⚠️

Async.Start(operation: Async<unit>, ?cancellationToken) : unit β†’ Performs the operation in the background (without blocking the calling thread) ⚠️ If an exception occurs, it is "swallowed"!

Async.StartImmediate(calc: Async<'T>, ?cancellationToken) : unit β†’ Performs the calculation in the calling thread!

Async.StartWithContinuations(calc, continuations..., ?cancellationToken) β†’ Like Async.RunSynchronously ⚠️ ... with 3 continuation callbacks: β†’ on success βœ…, exception πŸ’₯ and cancellation πŸ›‘

async { expression } block

A.k.a. Async workflow

Syntax for sequentially writing an asynchronous calculation β†’ The result of the calculation is wrapped in an Async object

Keywords

  • return β†’ final value of calculation β€’ unit if omitted

  • let! β†’ access to the result of an async sub-calculation (≃ await in Cβ™―)

  • use! β†’ like use (management of an IDisposable) + let!

  • do! β†’ like let! for async calculation without return (Async<unit>)

Fβ™― async function vs Cβ™― async method

Let's compare an Fβ™― async function...

... with its equivalent Cβ™― async method:

We can see the following equivalence regarding keywords and types:

Fβ™―
Cβ™―

Async<int> type

Task<int> type

async {...} block

async method keyword

let! keyword

var and await keywords

do! keyword

await keyword

Inappropriate use of Async.RunSynchronously

Async.RunSynchronously runs the calculation and returns the result BUT blocks the calling thread! Use it only at the "end of the chain" and not to unwrap intermediate asynchronous calculations! Use an async block instead.

Parallel calculations/operations

Async.Parallel

Async.Parallel(computations: Async<'T> seq, ?maxDegreeOfParallelism) : Async<'T array>

≃ Task.WhenAll : Fork-Join model

  • Fork: calculations run in parallel

    • Use the optional maxDegreeOfParallelism to throttle/limit the number of concurrently executing tasks ; it's like WithDegreeOfParallelism(throttle) in LINQ and ParallelEnumerable.

  • Wait for all calculations to finish

  • Join: aggregation of results

    • In the same order as calculations

⚠️ All calculations must return the same type!

Async.StartChild

Async.StartChild(calc: Async<'T>, ?timeoutMs: int) : Async<Async<'T>>

Allows several operations to be run in parallel β†’ ... whose results are of different types (β‰  Async.Parallel)

Used in async block with 2 let! per child calculation (cf. Async<Async<'T>>)

Shared cancellation πŸ“ β†’ Child calculation shares cancellation token with its parent calculation

Example:

We will test the following script in the console FSI, using the #time FSI directive (doc).

Let's first define a function delay β†’ which returns the specified value x β†’ after ms milliseconds

Timing results:

Operation
Real
CPU

inSeries

00:00:00.323

00:00:00.015

inParallel

00:00:00.218

00:00:00.031

β†’ inParallel is working, longing ~200ms vs ~300ms for inSeries. β†’ inParallel uses 2 times more CPU than inSeries.

Cancelling a task

Based on a default or explicit CancellationToken/Source:

  • Async.RunSynchronously(computation, ?timeout, ?cancellationToken)

  • Async.Start(computation, ?cancellationToken)

Trigger cancellation

  • Explicit token + cancellationTokenSource.Cancel()

  • Explicit token with timeout new CancellationTokenSource(timeout)

  • Default token: Async.CancelDefaultToken() β†’ OperationCanceledException πŸ’£

Check cancellation

  • Implicit: at each keyword in async block: let, let!, for...

  • Explicit local: let! ct = Async.CancellationToken then ct.IsCancellationRequested.

  • Explicit global: Async.OnCancel(callback)

Example:

Outputs

Summary

Adapted from πŸ”— Cancellation Tokens in F#, by AndrΓ© Silva

CT = Cancellation Token CTS = Cancellation Token Source

Keyword/Function
Shared CT
CT param
Linked CTS
Current thread
Use case

do!, let!... in async {}

Any?

Async.StartChild

Parallel operations

Async.Start

Fire & forget: send a message to a bus...

Async.StartImmediately

?

Async.RunSynchronously

Program root, scripting...

Last updated

Was this helpful?