Interop with .NET TPL

TPL : Task Parallel Library

Interaction with .NET libraries

Asynchronous libraries in .NET and the async/await C♯ pattern: → Based on TPL and the Task type

Gateways with asynchronous worflow F♯ :

  • Async.AwaitTask and Async.StartAsTask functions

  • task {} block

Gateway functions

Async.AwaitTask: Task<'T> -> Async<'T> → Consume an asynchronous .NET library in async block

Async.StartAsTask: Async<'T> -> Task<'T> → Launch an async calculation as a Task

let getValueFromLibrary param = async {
    let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
    return value
}

let computationForCaller param =
    async {
        let! result = getAsyncResult param
        return result
    }
    |> Async.StartAsTask

task {} block

Allows to consume an asynchronous .NET library directly, using a single Async.AwaitTask rather than 1 for each async method called.

💡 Available since F♯ 6 (before, we need Ply package nuget)

task {
    use client = new System.Net.Http.HttpClient()
    let! response = client.GetStringAsync("https://www.google.fr/")
    response.Substring(0, 300) |> printfn "%s"
}  // Task<unit>
|> Async.AwaitTask  // Async<unit>
|> Async.RunSynchronously

Async vs Task

1. Calculation start mode

Task = hot tasks → calculations started immediately❗

Async = task generators = calculation specification, independent of startup → Functional approach: no side-effects or mutations, composability → Control of startup mode: when and how 👍

2. Cancellation support

Task: by adding a CancellationToken parameter to async methods → Forces manual testing if token is canceled = tedious + error prone❗

Async: automatic support in calculations - token to be provided at startup 👍

Recommendation for async function in F♯

C♯ async applied at a method level ≠ F♯ async defines an async block, not an async function

Recommendation: » Put the entire body of the async function in an async block.

// ❌ Avoid
let workThenWait () =
    Thread.Sleep(1000)
    async { do! Async.Sleep(1000) } // Async only in this block 🧐

// ✅ Prefer
let workThenWait () = async {
    Thread.Sleep(1000)
    printfn "work"
    do! Async.Sleep(1000)
}

Pitfalls of the async/await C♯ pattern

Pitfall 1 - Really asynchronous?

In C♯: method async remains on the calling thread until the 1st await → Misleading feeling of being asynchronous throughout the method

async Task WorkThenWait() {
    Thread.Sleep(1000); // ⚠️ Blocks calling thread !
    await Task.Delay(1000); // Really async from here 🤔
}

Pitfall 2 - Omit the await

async Task PrintAfterOneSecond(string message) {
    await Task.Delay(1000);
    Console.WriteLine($"[{DateTime.Now:T}] {message}");
}

async Task Main() {
    PrintAfterOneSecond("Before"); // ⚠️ Missing `await`→ warning CS4014
    Console.WriteLine($"[{DateTime.Now:T}] After");
    await Task.CompletedTask;
}

Compiles but returns unexpected result: After before Before

[11:45:27] After
[11:45:28] Before

This pitfall is also present in F♯:

let printAfterOneSecond message = async {
    do! Async.Sleep 1000
    printfn $"[{DateTime.Now:T}] {message}"
}

async {
    printAfterOneSecond "Before" // ⚠️ Missing `do!` → warning FS0020
    printfn $"[{DateTime.Now:T}] After"
} |> Async.RunSynchronously

Compiles but returns another unexpected result: no Before at all ⁉️

[11:45:27] After

Compilation warnings

The previous examples compile but with big warnings!

C♯ warning CS4014 message:

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the await operator...

F♯ warning FS0020 message:

The result of this expression has type Async<unit> and is implicitly ignored. Consider using ignore to discard this value explicitly...

Recommendation: be sure to always handle this type of warnings! This is even more crucial in F♯ where compilation can be tricky.

Async.Parallel

Thread-safety

Impure functions can be not thread-safe, for instance if they mutate a shared object like the Console.

It's possible to make them thread-safe using the lock function:

open System
open System.Threading

let printColoredMessage =
    let lockObject = obj ()

    fun (color: ConsoleColor) (message: string) ->
        lock lockObject (fun () ->
            Console.ForegroundColor <- color
            printfn $"%s{message} (thread ID: %i{Thread.CurrentThread.ManagedThreadId})"
            Console.ResetColor())

[ ConsoleColor.Red
  ConsoleColor.Green
  ConsoleColor.Blue ]
|> List.randomShuffle
|> List.indexed
|> List.map (fun (i, color) -> async { printColoredMessage color $"Message {i}" })
|> Async.Parallel
|> Async.RunSynchronously

Results in the console (example):

🔗 Lock function documentation

Last updated

Was this helpful?