Atomics
An ordinary load or store may be split, reordered or removed by the compiler and the processor. That is harmless while one thread owns the memory, and wrong the moment two threads share it. The atomic operations in Core are compiled to the instructions each architecture provides for shared memory, so no thread ever sees one half done.
import Core::{ AtomicFetchAdd, AtomicLoad, MemoryOrder };
var Hits: uint64 = 0;
func Record() {
AtomicFetchAdd<uint64>(@Hits, 1, MemoryOrder::Relaxed);
}
func Total() -> uint64 {
return AtomicLoad<uint64>(@Hits, MemoryOrder::Acquire);
}
They act on one value of 8, 16, 32 or 64 bits — an integer, a bool or a pointer — through its address, which must be aligned to its size. Every thread that touches the value must reach it only through atomic operations. The Rux/Atomic package wraps them in an Atomic<T> type; they live in Core because only the compiler can emit them.
Operations
| Intrinsic | Does, as one indivisible step | Returns |
|---|---|---|
AtomicLoad<T>(address, order) | reads the value | the value |
AtomicStore<T>(address, value, order) | writes value | nothing |
AtomicExchange<T>(address, value, order) | writes value | the value it replaced |
AtomicCompareExchange<T>(address, expected, desired, success, failure) | writes desired if the value is expected | the value it found |
AtomicFetchAdd, AtomicFetchSub | adds or subtracts, wrapping on overflow (integers only) | the value before |
AtomicFetchAnd, AtomicFetchOr, AtomicFetchXor | combines bitwise (integers and bool) | the value before |
AtomicCompareExchange wrote exactly when the value it returns equals expected, so a retry loop starts again from what was really there. It never fails spuriously.
Memory orderings
Every operation takes a MemoryOrder, which says which of the thread's other memory operations may be reordered across it:
| Order | Guarantees |
|---|---|
Relaxed | the operation itself is atomic; nothing around it is held back — right for a counter no other memory depends on |
Acquire | later operations are not moved before this load, so what a releasing thread wrote before its release is visible |
Release | earlier operations are not moved after this store, so they are visible to a thread that acquires what it stores |
AcquireRelease | both, for a read-modify-write operation that reads and writes |
SequentiallyConsistent | AcquireRelease, and one order of every such operation that all threads agree on |
The weakest ordering that keeps a program correct is also the fastest, and a stronger one is never wrong. When in doubt, use SequentiallyConsistent.
The order is written at the call as a MemoryOrder case, because the compiler chooses the instructions from it while compiling. A value computed at run time is refused:
error: the memory order of 'AtomicLoad' must be written as a 'MemoryOrder' case
help: write the order at the call, such as 'MemoryOrder::Acquire'
A load cannot release and a store cannot acquire, so AtomicLoad takes only Relaxed, Acquire or SequentiallyConsistent, and AtomicStore only Relaxed, Release or SequentiallyConsistent:
error: 'AtomicStore' cannot use memory order 'Acquire'
note: a store only writes, so it cannot acquire
For AtomicCompareExchange, success orders the operation when it writes and failure when it only reads; failure may not be Release or AcquireRelease, nor acquire more than success does.
Fences and spin hints
Fence(order) orders the thread's memory operations without touching memory, for code that pairs relaxed operations with one ordering point. SpinHint() tells the processor the thread is waiting in a loop for another thread: it saves power and lets a sibling hardware thread run, and has no effect on memory. A loop that may wait long should block instead, through the Rux/Sync package.
On each architecture
On x86-64, loads and weaker stores are plain mov instructions and the other operations are lock-prefixed. On AArch64, the acquire and release forms of loads and stores are used, and read-modify-write operations run in exclusive-access loops.
See also
- Module variables — storage every thread shares, and
thread varfor storage each thread owns - Layout — sizes and alignments