Here is a recap on Async and Task expressions in F# These types of expressions allow us to use the .NET task system for concurrent and parallel programming.

1. Brief intro to concurrency

Concurrent programming naturally becomes relevant when moving from small programs into full scale software projects, where various tasks and event listeners need to run simultaneously.

There are many possible examples. To name a few:

  • A loading bar and UI has to remain alive while large tasks are completed.
  • A web server has to continue to listen on a network port while requests are fulfilled.
  • Modern video games run a large amount of concurrent foreground and background tasks on CPU as well as GPU.

1.1 Concurrency in .NET and F#

.NET C# code makes extensive use of the Task and Task<TResult> class. With special syntax declaring async methods and functions, these methods return a task instance to control task scheduling and access to results where required.

In this small C# code example, a counter is updated on a separate thread:

var counter = 0;

// Run Increment as a concurrent task.
var myTask = Task.Run(() => Increment(100_000_001));

for (var i = 0; i < 3; i++) // See the task progress.
{
    Thread.Sleep(1); // 1 ms
    Console.WriteLine(counter);
}

await myTask; // Wait for completion. 
Console.WriteLine(counter);
return;

void Increment(int n)
{ 
    for (var i = 0; i < n; i++) counter++;
}

In F#, Task expressions are provided to allow .NET tasks to be authored in F#. One problem with Task expressions is that they do not support tail call optimization.

For this reason Async expressions are the main way to write asynchronous code in F#. Async expressions support tail call optimization and can also be started as .NET tasks if needed.

2. Async expressions

This small F# code example implements a count to ten, with a 500 ms. of delay after each number is printed:

let countToAsync x =
    let rec counter i =
        async {
            if i = x then
                printfn "%d" i
            else
                printfn "%d" i
                do! Async.Sleep(500)
                return! counter (i+1)
        }
    counter 1
    
Async.RunSynchronously (countToAsync 10)

Even this small example contains many of the basic building blocks of Async expressions in F#. The following subsections explain in more detail.

2.1 Bindings and calls in expressions

  • An Async expression can call other Async expressions.
  • An Async expression can call itself recursively.
  • Recursive Async expressions can be tail call optimized. Similar to tail recursion.

The keywords let!, do!, and return! are used for calls to complete other Async expressions.

  • let! to wait and then bind the result of the call for other uses in this computation.
  • do! to only wait for the call to complete. I.e. for Async.Sleep or other Async<unit> calls.
  • return! to return the result of the call as the result of this computation.

This is similar to how the await keyword is used in the C# async/await framework.

Using let or return will instead bind the unrealized Async call. It is sometimes necessary to bind calls i.e. for parallel control flow:

let getValueAsync x =
    async {
        return x
    }

let helloWorldAsync =
    async {
        // These can also be written directly into a list literal.
        let s1 = getValueAsync "Hello"
        let s2 = getValueAsync ", "
        let s3 = getValueAsync "World"
        let s4 = getValueAsync "!"
        return! Async.Parallel [s1; s2; s3; s4]
    }
    
printfn "%A" (helloWorldAsync |> Async.RunSynchronously |> String.concat "")

2.2 Using Async expressions in F#

These are the fundamental functions for using Async expressions:

Async.RunSynchronously

A Async<'a> -> 'a function in its most basic use case.

(In more advanced cases this function can also takes timeout and cancellation token params.)

Starts an Async expression, waits for it to complete, and returns the result.

Async.Start

A Async<'a> -> unit function in its most basic use case.

(In more advanced cases this function can also takes timeout and cancellation token params.)

Starts an Async expression without waiting for the result. (Run the expression asynchronously.)

This can be used for calls that need to run concurrently with the main program, such as a Mailbox Processor agent.

Take the initial example. If I use this command to start the counter instead of Async.RunSynchronously the program would exit before the count to ten completes.

Async.Parallel

A Async<'a> seq -> Async<'a array> function.

Converts a sequence of computations into one large Async expression, which will run the computations in parallel. (Multi-threading. Tasks may complete in a semi-random order.)

The result of the expression is an array of computation results.

Async.Sequential

A Async<'a> seq -> Async<'a array> function.

Converts a sequence of computations into one large Async expression, which will run the computations sequentially. (One after the other on a single thread.)

The result of the expression is an array of computation results.

Async.Ignore

A Async<'a> -> Async<unit> function.

For use i.e. on a sequential or parallel task. The resulting Async expression will ignore the result even when using Async.RunSynchronously afterwards to execute it.

Async.Sleep

A int -> Async<unit> function.

Returns an Async<unit> call. This will wait for a given number of milliseconds when called. See the initial example for a use case.

3. Larger example: Message-based concurrency

To finish off this article. Here is a larger example using the MailboxProcessor agent found in F#.

3.1 Mailbox implementation basics

In this example, I have used a Mailbox Processor agent to implement an actual simple mailbox.

  • Mail can be received from other running Async calls or the main thread.
  • A TakeAllMail message gets all the stored mail through a reply channel and empties the box.
/// Types of messages that this mailbox will accept.
type mail =
    | SendMessage of string
    | TakeAllMail of AsyncReplyChannel<string list>

/// A simple mailbox using a mailbox processor.
let mailbox (mbox : MailboxProcessor<mail>) =
    let rec messageLoop (contents : string list) =
        async {
            let! mail = mbox.Receive()
            match mail with
            | SendMessage text ->
                return! messageLoop (text :: contents)
            | TakeAllMail replyChannel ->
                replyChannel.Reply contents
                return! messageLoop []
        } // Both cases end in tail-recursive call.
        
    messageLoop [] // Mailbox initial state.
  • The messageLoop processes incoming messages. mBox.Receive() blocks until a message is received.
  • Only one message is received and processed at a time. No messages are dropped even if posting to the mailbox in parallel.
  • The only state in the mailbox is the contents parameter of the message loop.
  • Each union case match of mail makes a new call to messageLoop with the updated state.
  • The new call is made in a way that is tail call optimizable. The previous call is discarded from the stack.

3.2 Using the mailbox

Here is some code to test the mailbox, where Async expressions are used to send messages in parallel:

/// Bind of a new mailbox instance.
let myMailbox = MailboxProcessor.Start mailbox

/// List comprehension - Ten messages to be sent.
let mailsToSend =
    [ for x in 1..10 ->
        async { myMailbox.Post(SendMessage($"Hello World mail %d{x}!")) } ]

// Send mails in parallel.
// Ignore the result (unit array).
// May semi-randomize the ordering.
mailsToSend |> Async.Parallel |> Async.RunSynchronously |> ignore

let response1 = myMailbox.PostAndReply(TakeAllMail)
printfn "%A" response1
// Remember that union case constructors are functions.
// 'TakeAllMail' is of type 'AsyncReplyChannel<string list> -> mail'
// for the channel to be wrapped as a type suitable for myMailbox.

// To show that mailbox is emptied as intended:
myMailbox.Post(SendMessage("Mailed after myMailbox was emptied."))
let response2 = myMailbox.PostAndReply(TakeAllMail)
printfn "%A" response2

Further Reading