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

Atomics

An atomic value can be read and updated by several threads at once without a data race: every operation on it happens as one indivisible step. Pepper provides this through async.Atomic<T>, the equivalent of C++ std::atomic<T>.

Threads themselves, and the rules for what they can share, are in Threads and shared data.

Atomic<T>

Atomic<T> belongs to the async module, with the other types threads share: import async and write async.Atomic<T>, or bring the name in with import [Atomic] from async. The Atomic reference documents every method.

import async;

main(): int {
    var a = async.Atomic<int>(41);
    assert(a.load() == 41, "the initial value");

    a.store(1);
    assert(a.load() == 1, "store replaces the value");

    # The fetch_ operations return the value from before the change.
    assert(a.fetch_add(5) == 1, "fetch_add returns the old value");
    assert(a.load() == 6, "1 + 5");
    assert(a.fetch_sub(2) == 6, "fetch_sub returns the old value");
    assert(a.load() == 4, "6 - 2");

    var flags = async.Atomic<uint>(12);          # 0b1100
    flags.fetch_and(10);                   # 0b1000
    assert(flags.load() == 8, "fetch_and");
    flags.fetch_or(1);                     # 0b1001
    assert(flags.load() == 9, "fetch_or");
    flags.fetch_xor(9);                    # 0b0000
    assert(flags.load() == 0, "fetch_xor");

    # compare_exchange stores only if the current value is the expected one.
    assert(a.compare_exchange(4, 10), "4 matched, so 10 is stored");
    assert(!a.compare_exchange(4, 20), "10 does not match 4");
    assert(a.load() == 10, "the failed exchange changed nothing");

    # exchange always stores, and returns what was there.
    assert(a.exchange(3) == 10, "exchange returns the old value");
    assert(a.load() == 3, "and stores the new one");

    assert(a.is_lock_free(), "an int fits a native atomic instruction");
    return 0;
}
MethodWhat it does
Atomic<T>(value)Creates an atomic holding value (zero, or null for a struct type, if you leave it out).
load()Reads the value.
store(v)Replaces the value with v.
compare_exchange(expected, desired)Stores desired only if the value is currently expected; returns whether it did.
exchange(v)Stores v and returns the value it replaced.
fetch_add(v), fetch_sub(v)Adds or subtracts v; returns the value from before. Overflow wraps around.
fetch_and(v), fetch_or(v), fetch_xor(v)Combines the value bitwise with v; returns the value from before.
is_lock_free()Reports whether the operations use native atomic instructions.

Each operation compiles to a native atomic instruction. load has acquire ordering, store has release ordering, and compare_exchange and the fetch_ operations are acquire-release. exchange is a compare_exchange retry loop.

Some details differ from C++:

  • compare_exchange does not update expected when it fails. Call load() to get the current value before you retry.
  • The comparison is bitwise. For a floating-point T, 0.0 and -0.0 are different values. For a struct T, it compares references (whether both are the same object), not the objects’ contents.
  • The fetch_ operations are for integer types. Calling one on a floating-point or struct T compiles, but stops the program when it runs.
  • is_lock_free() is true for any number type of 8 bytes or less, and for any struct type.
  • T must be thread-safe. Atomic is a [Shared] struct, so it is declared Atomic<T: Shared>: T is a number, bool, an enum, a [Shared] struct, or another type in What threads can share. Atomic<String> is a compile error at String (see the Shared constraint). A tuple is thread-safe, but its value cannot be stored atomically, so Atomic of a tuple is an error too, and so is Atomic of a trait type (see Weak references and atomics).

Common patterns

import async;

# An Atomic can only hold a thread-safe struct: one declared [Shared], whose
# fields are const and thread-safe.
[Shared]
struct Config {
    const version: int;
    construct(this.version = 0) {}
}

# A module-scope Atomic that every function can share.
const next_id = async.Atomic<long>(0);

new_id(): long {
    return next_id.fetch_add(1) + 1;
}

# The compare-and-swap retry loop: build any read-modify-write update.
double_it(async.Atomic<int> value) {
    var current = value.load();
    while (!value.compare_exchange(current, current * 2)) {
        current = value.load();
    }
}

main(): int {
    assert(new_id() == 1, "first id");
    assert(new_id() == 2, "second id");

    var n = async.Atomic<int>(21);
    double_it(n);
    assert(n.load() == 42, "doubled");

    # Copies of an Atomic share one slot, because Atomic is a struct.
    var same = n;
    same.store(7);
    assert(n.load() == 7, "both names see the store");

    # An Atomic can hold a struct reference.
    var first = Config(1);
    var current = async.Atomic<Config>(first);
    assert(current.load().version == 1, "load returns the reference");

    # compare_exchange compares references: the same object, not equal contents.
    var lookalike = Config(1);
    var second = Config(2);
    assert(!current.compare_exchange(lookalike, second), "a different object");
    assert(current.compare_exchange(first, second), "the same object");
    assert(current.load().version == 2, "swapped");

    var old = current.exchange(first);
    assert(old.version == 2, "exchange hands back the old reference");
    return 0;
}
  • A shared counter. fetch_add(1) hands out unique, increasing numbers, even when many threads call it at once.
  • A compare-and-swap loop. To apply any update that the fetch_ operations do not cover, load the current value, compute the new one, and compare_exchange; if another thread changed the value in between, the exchange fails and you try again with a fresh load().
  • Sharing an Atomic. Atomic is a [Shared] struct, so copying one copies a reference: every copy shares the same slot, on any thread. To share a value, pass the Atomic around or keep it in a module-scope variable.
  • Atomic struct references. An Atomic<T> whose T is a [Shared] struct type holds a counted reference, and follows the same rules as any other reference (see Memory and reference counting): store and a successful compare_exchange take a reference to the new value and release the one they replace, and load and exchange give you your own reference. The swap in the slot is itself atomic, so concurrent stores never leak or release a value twice. When the Atomic itself is destroyed, it releases the reference it holds.

Atomics and threads

Atomic<T> is how threads share a value that changes. Keep it in a module-scope variable, or in a const field of a [Shared] struct, and every thread sees the same slot. Threads and shared data shows it together with Thread.spawn.