Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Count with atomics

A shared borrow lets several workers read the same data, but not change it. An atomic from std.atomic is the exception: its methods update it through a shared reference, one indivisible step at a time:

from std.atomic import Atomic, Ordering

def count(hits: &Atomic[u64], rounds: i64) -> ():
    for _ in range(rounds):
        hits.fetch_add(1, Ordering.Relaxed)

def main() -> Result[(), SpawnError[()] | IoError]:
    hits = Atomic[u64](0)
    with spawn(count, &hits, 100000)?, spawn(count, &hits, 100000)?:
        pass
    print(hits.load(Ordering.Acquire))?
    Ok(())
200000

Atomic[u64](0) stores the counter inline in main’s frame; nothing is allocated. fetch_add returns the value it replaced. It stops the program on overflow, before storing anything.

Every operation names an Ordering. Relaxed only makes the update itself indivisible. Release on a store and Acquire on a load make the writes before the store visible to the thread that loads it. Use SeqCst when unsure. The compiler rejects orderings an operation cannot use:

from std.atomic import Atomic, Ordering

def main() -> ():
    ready = Atomic[bool](False)
    ready.store(True, Ordering.Acquire)
store does not accept Ordering.Acquire; use Ordering.Relaxed, Ordering.Release, or Ordering.SeqCst

A class can hold atomics as fields, so one borrow shares several of them. Atomics are not values to print or compare, so read each one with load:

from std.atomic import Atomic, Ordering

class Stats:
    seen: Atomic[u32]
    largest: Atomic[i64]

def record(stats: &Stats, start: i64) -> Result[(), OverflowError[i64]]:
    for value in range(start, start + 1000):
        stats.seen.fetch_add(1, Ordering.Relaxed)
        stats.largest.fetch_max(value, Ordering.Relaxed)
    Ok(())

def main() -> Result[(), SpawnError[()] | IoError | OverflowError[i64]]:
    stats = Stats(seen=Atomic(0), largest=Atomic(i64.MIN))
    with spawn(record, &stats, 0)? as first, spawn(record, &stats, 5000)? as second:
        first.join()?
        second.join()?
    print(stats.seen.load(Ordering.Acquire))?
    print(stats.largest.load(Ordering.Acquire))?
    Ok(())
2000
5999

compare_exchange replaces a value only if it still equals the one you expect, and returns Err with the actual value otherwise. See std.atomic for every method.