F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • πŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • πŸ”Quiz
    • πŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • πŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • πŸš€Computation expression (CE)
    • πŸš€CE theoretical basements
    • πŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • πŸš€Fold function
    • πŸ“œRecap
    • πŸ•ΉοΈExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • πŸ”Quiz
    • πŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • πŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • πŸ”Quiz
    • πŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Purpose
  • Async<'T> type
  • Methods returning an Async object
  • Run an async calculation
  • async { expression } block
  • Fβ™― async function vs Cβ™― async method
  • Inappropriate use of Async.RunSynchronously
  • Parallel calculations/operations
  • Async.Parallel
  • Async.StartChild
  • Cancelling a task
  • Summary

Was this helpful?

Edit on GitHub
  1. Asynchronous programming

Asynchronous workflow

PreviousRecapNextInterop with .NET TPL

Last updated 19 days ago

Was this helpful?

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 () β†’ AsyncRead(buffer: byte[], ?offset: int, ?count: int) : Async<int> β†’ AsyncWrite(buffer: byte[], ?offset: int, ?count: int) : Async<unit>

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 β†’ Perform the operation in the background (without blocking the calling thread) ⚠️ If an exception occurs, it is "swallowed"!

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

Async.StartWithContinuations(calc, continuations..., ?cancellationToken) β†’ Ditto Async.RunSynchronously ⚠️ ... with 3 callbacks of continuation: β†’ 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! β†’ ditto use (management of an IDisposable) + let!

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

let repeat (computeAsync: int -> Async<string>) times = async {
    for i in [ 1..times ] do
        printf $"Start operation #{i}... "
        let! result = computeAsync i
        printfn $"Result: {result}"
}

let basicOp (num: int) = async {
    do! Async.Sleep 150
    return $"{num} * ({num} - 1) = {num * (num - 1)}"
}

repeat basicOp 5 |> Async.RunSynchronously

// Start operation #1... Result: 1 * (1 - 1) = 0
// Start operation #2... Result: 2 * (2 - 1) = 2
// Start operation #3... Result: 3 * (3 - 1) = 6
// Start operation #4... Result: 4 * (4 - 1) = 12
// Start operation #5... Result: 5 * (5 - 1) = 20

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

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

// string -> Async<int>
let getLength url = async {
    let! html = fetchAsync url
    do! Async.Sleep 1000
    return html.Length
}

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

public async Task<int> GetLength(string url) {
    var html = await FetchAsync(url);
    await Task.Delay(1000);
    return html.Length;
}

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.

// ❌ Avoid
let a = calcA |> Async.RunSynchronously
let b = calcB a |> Async.RunSynchronously
calcC b

// βœ… Favor
async {
    let! a = calcA
    let! b = calcB a
    return calcC b
}
|> Async.RunSynchronously

Parallel calculations/operations

Async.Parallel

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

  • 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!

let downloadSite (site: string) = async {
    do! Async.Sleep (100 * site.Length)
    printfn $"{site} βœ…"
    return site.Length
}

[ "google"; "msn"; "yahoo" ]
|> List.map downloadSite  // string list
|> Async.Parallel         // Async<string[]>
|> Async.RunSynchronously // string[]
|> printfn "%A"

// msn βœ…
// yahoo βœ…
// google βœ…
// [|6; 3; 5|]

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:

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

let delay (ms: int) x = async {
    do! Async.Sleep ms
    return x
}

let inSeries = async {
    let! result1 = "a" |> delay 100
    let! result2 = 123 |> delay 200
    return (result1, result2)
}

#time "on"
inSeries |> Async.RunSynchronously    // Real: 00:00:00.317, ...
#time "off"

// --> Timing now on

// Real: 00:00:00.323, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
// val it: string * int = ("a", 123)


// --> Timing now off

//-------------------------

let inParallel = async {
    let! child1 = "a" |> delay 100 |> Async.StartChild
    let! child2 = 123 |> delay 200 |> Async.StartChild
    let! result1 = child1
    let! result2 = child2
    return (result1, result2)
}

#time "on"
inParallel |> Async.RunSynchronously  // Real: 00:00:00.205, ...
#time "off"

// --> Timing now on

// Real: 00:00:00.218, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
// val it: string * int = ("a", 123)


// --> Timing now off

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:

let sleepLoop = async {
    let stopwatch = System.Diagnostics.Stopwatch()
    stopwatch.Start()
    let log message = printfn $"""   [{stopwatch.Elapsed.ToString("s\.fff")}] {message}"""

    use! __ = Async.OnCancel (fun () ->
        log $"  Cancelled ❌")

    for i in [ 1..5 ] do
        log $"Step #{i}..."
        do! Async.Sleep 500
        log $"  Completed βœ…"
}

open System.Threading

printfn "1. RunSynchronously:"
Async.RunSynchronously(sleepLoop)

printfn "2. Start with CancellationTokenSource + Sleep + Cancel"
use manualCancellationSource = new CancellationTokenSource()
Async.Start(sleepLoop, manualCancellationSource.Token)
Thread.Sleep(1200)
manualCancellationSource.Cancel()

printfn "3. Start with CancellationTokenSource with timeout"
use cancellationByTimeoutSource = new CancellationTokenSource(1200)
Async.Start(sleepLoop, cancellationByTimeoutSource.Token)
Outputs
1. RunSynchronously:
   [0.009] Step #1...
   [0.532]   Completed βœ…
   [0.535] Step #2...
   [1.037]   Completed βœ…
   [1.039] Step #3...
   [1.543]   Completed βœ…
   [1.545] Step #4...
   [2.063]   Completed βœ…
   [2.064] Step #5...
   [2.570]   Completed βœ…
2. Start with CancellationTokenSource + Sleep + Cancel
   [0.000] Step #1...
   [0.505]   Completed βœ…
   [0.505] Step #2...
   [1.011]   Completed βœ…
   [1.013] Step #3...
   [1.234]   Cancelled ❌
3. Start with CancellationTokenSource with timeout
... idem 2.

Summary

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

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

≃ Task.WhenAll :

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

Adapted from πŸ”— , by AndrΓ© Silva

doc
doc
Fork-Join model
doc
Cancellation Tokens in F#