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;
}
| Method | What 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_exchangedoes not updateexpectedwhen it fails. Callload()to get the current value before you retry.- The comparison is bitwise. For a floating-point
T,0.0and-0.0are different values. For a structT, 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 structTcompiles, 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.Tmust be thread-safe.Atomicis a[Shared]struct, so it is declaredAtomic<T: Shared>:Tis a number,bool, an enum, a[Shared]struct, or another type in What threads can share.Atomic<String>is a compile error atString(see theSharedconstraint). A tuple is thread-safe, but its value cannot be stored atomically, soAtomicof a tuple is an error too, and so isAtomicof 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, andcompare_exchange; if another thread changed the value in between, the exchange fails and you try again with a freshload(). - Sharing an Atomic.
Atomicis a[Shared]struct, so copying one copies a reference: every copy shares the same slot, on any thread. To share a value, pass theAtomicaround or keep it in a module-scope variable. - Atomic struct references. An
Atomic<T>whoseTis a[Shared]struct type holds a counted reference, and follows the same rules as any other reference (see Memory and reference counting):storeand a successfulcompare_exchangetake a reference to the new value and release the one they replace, andloadandexchangegive you your own reference. The swap in the slot is itself atomic, so concurrent stores never leak or release a value twice. When theAtomicitself 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.