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)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())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 Lock10 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))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 withAdd,Exchange, andCompareExchange.Mutex<T>holds a Sendable value that only aLockblock reaches, one thread at a time.- A Lock is released however its block ends.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.