Skip to content
FutureBlitz

    Install

    Concurrency, 4 of 8

    Mutex and Atomic

    Share counters between threads with Atomic, and larger data with a Mutex and Lock.

    What you’ll build

    A parallel loop over ten thousand numbers that counts them in two shared counters, and keeps a small record of what it saw behind a lock. Then a flag that only one thread can claim.

    Why a plain Global won’t do

    A Global that any thread could change would be a data race: two threads adding to it at once can lose an update. So a plain Global is the main thread’s alone, and work on another thread that uses one is an error:

    Import std.core
    
    Global total = 0
    
    Parallel For i = 1 To 10
        total += i
    End For
    Print(total)
    $ fb run main.fb
       Compiling main.fb
    main.fb:5:1: error: a Parallel For's body runs on other threads, but it uses the Global 'total', which only the main thread can, unless it's an Atomic or a Mutex
      |
    5 | Parallel For i = 1 To 10
      | ^^^^^^^^
    main.fb:6:5: note: here
      |
    6 |     total += i
      |     -----
    error: couldn't build main.fb because of 1 error

    Atomic

    An Atomic<T> holds an integer or a Bool that every thread can change safely. Add(amount) adds to it in one step that no other thread can come between, and Load() reads it.

    Import std.core
    
    Global checked = Atomic<Int>(0)
    Global evens = Atomic<Int>(0)
    
    Parallel For i = 1 To 10_000
        checked.Add(1)
        If i % 2 == 0 Then evens.Add(1)
    End For
    
    Print(checked.Load(), evens.Load())
    Output
    10000 5000

    Every one of the ten thousand passes counts, whatever thread it ran on. An integer’s Atomic also has Subtract, and every Atomic has Store, Exchange, and CompareExchange.

    Mutex and Lock

    A value bigger than one number goes in a Mutex<T>, whose T is Sendable, like a Struct of numbers. Its value is reached only inside Lock m As value ... End Lock, and while one thread is inside, another’s Lock of the same Mutex waits.

    Import std.core
    
    Struct Stats
        Field Count: Int
        Field Largest: Int
    End Struct
    
    Global stats = Mutex(Stats())
    
    Parallel For i = 1 To 10_000
        If i % 1000 == 0
            Lock stats As s
                s.Count += 1
                If i > s.Largest Then s.Largest = i
            End Lock
        End If
    End For
    
    Lock stats As s
        Print(s.Count, s.Largest)
    End Lock
    Output
    10 10000

    Inside the block, s is the Mutex’s own value, not a copy, so changing s changes it. The lock is released at End Lock, and also when the block is left early, by Return, Break, or an error. Mutex(Stats()) takes its T from the value; Mutex<Stats>(Stats()) says it.

    Keep a Lock’s block short: every other thread that wants the Mutex waits while it runs. A thread that Locks a Mutex it already holds panics, since it would wait for itself forever, and a Lock block can’t Await.

    Claiming a flag

    CompareExchange(expected, desired) sets the Atomic to desired only if it holds expected, and says whether it did. Of many threads that try it at once, exactly one succeeds, which makes it a way to do something once.

    Import std.core
    
    Global started = Atomic<Bool>(False)
    
    Print(started.CompareExchange(False, True), started.Load())
    Print(started.CompareExchange(False, True))
    Output
    True True
    False

    The first call finds False, sets True, and gives True. The second finds True already, and changes nothing.

    An Atomic or a Mutex stays where it’s made: a Global, a Static Field, or a field of a Class. Code can pass one ByRef, but never copies one, so every thread shares the same one.

    What you learned

    • A plain Global belongs to the main thread; other threads share an Atomic or a Mutex instead.
    • Atomic<T> holds an integer or Bool, changed in single steps with Add, Exchange, and CompareExchange.
    • Mutex<T> holds a Sendable value that only a Lock block reaches, one thread at a time.
    • A Lock is released however its block ends.

    Report an issue with this page or with FutureBlitz on GitHub.