Atomic Counters in F#

Our primary mechanism for managing state in F# is typically through immutable data structures and functional programming patterns. However, there are scenarios where we need to manage shared mutable state across multiple threads. In this example, we’ll look at using atomic operations for managing a counter accessed by multiple threads.

open System
open System.Threading
open System.Threading.Tasks

let main () =
    // We'll use an atomic integer to represent our (always-positive) counter.
    let ops = ref 0L

    // We'll start 50 tasks that each increment the counter exactly 1000 times.
    let tasks = 
        [1..50]
        |> List.map (fun _ ->
            Task.Run(fun () ->
                for _ in 1..1000 do
                    // To atomically increment the counter we use Interlocked.Increment
                    Interlocked.Increment(ops) |> ignore
            )
        )

    // Wait until all the tasks are done.
    Task.WaitAll(tasks |> Array.ofList)

    // It's safe to read the value of 'ops' here as all tasks have completed
    printfn "ops: %d" !ops

main ()

In this F# version:

  1. We use ref to create a mutable reference cell for our counter, initialized to 0L (long integer).

  2. Instead of goroutines, we use F#’s Task.Run to create and run tasks concurrently.

  3. We use Interlocked.Increment for atomic incrementation of the counter. This is equivalent to the Add method used in the original example.

  4. We use Task.WaitAll to wait for all tasks to complete, which is similar to the WaitGroup.Wait() in the original.

  5. After all tasks are complete, we can safely read the value of ops using the ! operator to dereference the ref cell.

When you run this program, you should see:

ops: 50000

We expect to get exactly 50,000 operations. If we had used a non-atomic integer and incremented it with normal addition, we’d likely get a different number, changing between runs, because the tasks would interfere with each other.

This example demonstrates how to use atomic operations in F# for safe concurrent access to shared mutable state. However, in idiomatic F# programming, you would typically prefer immutable data structures and message-passing concurrency models when possible.