zig-skills/references/std-atomic.md

420 lines
13 KiB
Markdown

# std.atomic - Atomic Operations Reference (Zig 0.16.0)
Lock-free atomic operations for concurrent programming. Wraps Zig's atomic builtins with a type-safe interface.
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
Lock-free atomics do not need `std.Io`. Blocking synchronization should use `std.Io.Mutex`, `std.Io.Condition`, `std.Io.Semaphore`, `std.Io.RwLock`, `std.Io.Event`, or `std.Io.Group` when used in I/O-aware code.
## Table of Contents
- [Module Structure](#module-structure)
- [Atomic Value Wrapper](#atomic-value-wrapper)
- [Atomic Operations](#atomic-operations)
- [Atomic Ordering](#atomic-ordering)
- [Spin Loop Hint](#spin-loop-hint)
- [Cache Line Size](#cache-line-size)
- [Common Patterns](#common-patterns)
## Module Structure
```zig
std.atomic.Value(T) // Atomic wrapper for T (integers, enums, floats, bools, pointers)
std.atomic.Mutex // Non-blocking/spinning atomic mutex
std.atomic.spinLoopHint() // CPU hint for spin-wait loops
std.atomic.cache_line // Target-based cache-line estimate (comptime constant)
```
## Atomic Value Wrapper
`std.atomic.Value(T)` wraps a value to enable atomic operations. Supported types: integers, enums, floats, bools, optional pointers.
### Creation
```zig
const std = @import("std");
// Initialize with value
var counter = std.atomic.Value(u64).init(0);
// Initialize in struct
const State = struct {
count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
flag: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
};
// Direct access (careful - not atomic!)
var x = std.atomic.Value(u32).init(10);
x.raw = 20; // Non-atomic write - use only when no concurrent access
```
### Basic Operations
```zig
var x = std.atomic.Value(u32).init(5);
// Load (atomic read)
const val = x.load(.acquire);
// Store (atomic write)
x.store(10, .release);
// Swap (exchange, returns old value)
const old = x.swap(20, .seq_cst); // old = 10, x = 20
```
## Atomic Operations
### Fetch-and-Modify Operations
All return the **previous** value before modification:
```zig
var x = std.atomic.Value(i32).init(10);
// Arithmetic
_ = x.fetchAdd(5, .seq_cst); // x = 15, returns 10
_ = x.fetchSub(3, .seq_cst); // x = 12, returns 15
_ = x.fetchMin(8, .seq_cst); // x = 8, returns 12
_ = x.fetchMax(20, .seq_cst); // x = 20, returns 8
// Bitwise
var bits = std.atomic.Value(u8).init(0b1100);
_ = bits.fetchAnd(0b1010, .seq_cst); // 0b1000
_ = bits.fetchOr(0b0011, .seq_cst); // 0b1011
_ = bits.fetchXor(0b1111, .seq_cst); // 0b0100
_ = bits.fetchNand(0b1100, .seq_cst); // ~(0b0100 & 0b1100) = ~0b0100
// Generic RMW (any AtomicRmwOp)
_ = x.rmw(.Add, 1, .seq_cst);
```
### Compare-and-Swap (CAS)
```zig
var x = std.atomic.Value(u32).init(100);
// Strong CAS - guaranteed to succeed if values match
const result = x.cmpxchgStrong(100, 200, .seq_cst, .seq_cst);
// result = null (success, x is now 200)
// result = 100 (failure, current value if mismatch)
// Weak CAS - may spuriously fail, use in loops
var current: u32 = x.load(.acquire);
while (x.cmpxchgWeak(current, current + 1, .acq_rel, .acquire)) |actual| {
current = actual; // Retry with actual value
}
```
**When to use which:**
- `cmpxchgStrong`: Single attempt, no retry loop
- `cmpxchgWeak`: In retry loops (more efficient on some architectures)
### Bit Operations
Individual bit manipulation, returning previous bit state:
```zig
var flags = std.atomic.Value(u32).init(0);
// Set bit (returns previous bit value: 0 or 1)
const was_set = flags.bitSet(3, .seq_cst); // Set bit 3
// Reset bit
const was_reset = flags.bitReset(3, .seq_cst); // Clear bit 3
// Toggle bit
const prev = flags.bitToggle(5, .seq_cst); // Flip bit 5
```
## Atomic Ordering
Memory orderings control synchronization guarantees. From `std.builtin.AtomicOrder`:
| Order | Guarantees | Use Case |
|-------|------------|----------|
| `.unordered` | No ordering, loads/stores only (no RMW) | Preventing torn reads/writes only (e.g., data inside SeqLock) |
| `.monotonic` | Coherent on same variable, reorderable with other atomics | Simple counters, progress indicators |
| `.acquire` | Subsequent reads/writes won't move before this | Loading shared data after flag check |
| `.release` | Prior reads/writes won't move after this | Publishing data before setting flag |
| `.acq_rel` | Both acquire and release | Read-modify-write on shared data |
| `.seq_cst` | Total order among all seq_cst operations | When multiple atomics must be globally ordered |
### Ordering Guidelines
```zig
// Producer-consumer pattern
var data: Data = undefined;
var ready = std.atomic.Value(bool).init(false);
// Producer thread
fn produce() void {
data = computeData(); // Write data first
ready.store(true, .release); // Then publish (release ensures order)
}
// Consumer thread
fn consume() void {
while (!ready.load(.acquire)) { // Acquire synchronizes with release
std.atomic.spinLoopHint();
}
useData(data); // Safe to read after acquire
}
```
**Rules of thumb:**
- `.monotonic` for counters that don't guard other data (can still reorder with other atomics)
- `.release` when publishing/storing data that others will read
- `.acquire` when consuming/loading data others published
- `.acq_rel` for RMW operations that both read and write shared state
- `.seq_cst` when multiple atomics must have a single global order visible to all threads (only orders with other seq_cst ops)
## Spin Loop Hint
`spinLoopHint()` tells the CPU it's in a spin-wait loop, improving power efficiency and SMT performance:
```zig
fn spinWait(flag: *std.atomic.Value(bool)) void {
while (!flag.load(.acquire)) {
std.atomic.spinLoopHint(); // Reduce power, yield to sibling threads
}
}
```
Representative architecture-specific behavior (the implementation contains additional target/feature cases):
- **x86/x86_64**: `pause` instruction
- **AArch64**: `isb` instruction
- **ARM**: feature-dependent yield/spin hint
- **RISC-V**: `pause` (Zihintpause extension)
- **Some unsupported targets**: No-op
## Cache Line Size
`cache_line` is a target-based estimate of cache-line size, useful when reducing false sharing. It is not a runtime query of the current CPU:
```zig
const cache_line = std.atomic.cache_line; // 64, 128, etc.
// Pad struct to avoid false sharing between threads
const PaddedCounter = struct {
value: std.atomic.Value(u64) align(cache_line) = std.atomic.Value(u64).init(0),
_padding: [cache_line - @sizeOf(std.atomic.Value(u64))]u8 = undefined,
};
// Per-thread counters without false sharing
const ThreadCounters = struct {
counters: [MAX_THREADS]PaddedCounter = [_]PaddedCounter{.{}} ** MAX_THREADS,
};
```
Representative values selected by the stdlib's target table (not a complete hardware guarantee):
- x86_64, AArch64: 128 bytes (big cores)
- ARM, MIPS: 32 bytes
- Most others: 64 bytes
## Common Patterns
### Thread-Safe Counter
```zig
const Counter = struct {
value: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
pub fn increment(self: *@This()) void {
_ = self.value.fetchAdd(1, .monotonic);
}
pub fn decrement(self: *@This()) void {
_ = self.value.fetchSub(1, .monotonic);
}
pub fn get(self: *const @This()) u64 {
return self.value.load(.monotonic);
}
};
```
### Reference Counting
```zig
const RefCounted = struct {
ref_count: std.atomic.Value(usize),
data: *Data,
pub fn retain(self: *@This()) void {
_ = self.ref_count.fetchAdd(1, .monotonic);
}
pub fn release(self: *@This()) void {
// Release ensures writes before release are visible to thread that sees 1
if (self.ref_count.fetchSub(1, .release) == 1) {
// Acquire synchronizes with all previous releases
_ = self.ref_count.load(.acquire);
self.destroy();
}
}
};
```
### Lock-Free Stack (Treiber Stack)
**Note:** This simplified implementation only supports single-consumer (one thread calling `pop()` at a time). Multiple concurrent poppers require ABA protection (hazard pointers, epoch-based reclamation, or tagged pointers) to safely read `node.next` without use-after-free.
```zig
fn Stack(comptime T: type) type {
return struct {
const Node = struct {
value: T,
next: ?*Node,
};
head: std.atomic.Value(?*Node) = std.atomic.Value(?*Node).init(null),
pub fn push(self: *@This(), node: *Node) void {
var current_head = self.head.load(.acquire);
while (true) {
node.next = current_head;
if (self.head.cmpxchgWeak(current_head, node, .release, .acquire)) |actual| {
current_head = actual;
} else {
break; // Success
}
}
}
// Single-consumer only! See note above.
pub fn pop(self: *@This()) ?*Node {
var current_head = self.head.load(.acquire);
while (current_head) |node| {
if (self.head.cmpxchgWeak(current_head, node.next, .acq_rel, .acquire)) |actual| {
current_head = actual;
} else {
return node; // Success
}
}
return null; // Empty
}
};
}
```
### Once Initialization (Double-Checked Locking)
```zig
var initialized = std.atomic.Value(bool).init(false);
var init_mutex: std.Io.Mutex = .init;
var global_resource: ?*Resource = null;
fn getResource(io: std.Io) *Resource {
// Fast path: already initialized
if (initialized.load(.acquire)) {
return global_resource.?;
}
// Slow path: initialize with lock
// This variant deliberately makes initialization uncancelable. Use
// `try init_mutex.lock(io)` in a cancelable function instead.
init_mutex.lockUncancelable(io);
defer init_mutex.unlock(io);
if (!initialized.load(.acquire)) {
global_resource = initializeResource();
initialized.store(true, .release);
}
return global_resource.?;
}
```
### Spin Lock
```zig
const SpinLock = struct {
locked: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
pub fn lock(self: *@This()) void {
while (self.locked.swap(true, .acquire)) {
while (self.locked.load(.monotonic)) {
std.atomic.spinLoopHint();
}
}
}
pub fn unlock(self: *@This()) void {
self.locked.store(false, .release);
}
pub fn tryLock(self: *@This()) bool {
return !self.locked.swap(true, .acquire);
}
};
```
### Progress Flag (SeqLock)
**Note:** `Data` must be a type that supports atomic load/store (integers, bools, enums, floats, pointers). For larger structs, use a pointer or different synchronization.
```zig
const SeqLock = struct {
seq: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
data: Data = .{},
// Single-writer only!
pub fn write(self: *@This(), new_data: Data) void {
// Odd sequence = write in progress
_ = self.seq.fetchAdd(1, .release);
@atomicStore(Data, &self.data, new_data, .unordered);
_ = self.seq.fetchAdd(1, .release);
}
pub fn read(self: *@This()) Data {
while (true) {
const seq1 = self.seq.load(.acquire);
if (seq1 & 1 != 0) {
std.atomic.spinLoopHint();
continue; // Write in progress
}
const data = @atomicLoad(Data, &self.data, .unordered);
const seq2 = self.seq.load(.acquire);
if (seq1 == seq2) return data;
std.atomic.spinLoopHint();
}
}
};
```
### Barrier Synchronization
```zig
const Barrier = struct {
counter: std.atomic.Value(usize),
generation: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
total: usize,
pub fn init(count: usize) @This() {
return .{
.counter = std.atomic.Value(usize).init(count),
.total = count,
};
}
pub fn wait(self: *@This()) void {
const gen = self.generation.load(.acquire);
if (self.counter.fetchSub(1, .acq_rel) == 1) {
// Last thread to arrive
self.counter.store(self.total, .release);
_ = self.generation.fetchAdd(1, .release);
} else {
// Wait for generation to change
while (self.generation.load(.acquire) == gen) {
std.atomic.spinLoopHint();
}
}
}
};
```
## See Also
- **[std.Io synchronization](std-io.md)** - Blocking `Mutex`, `RwLock`, `Condition`, `Semaphore`, `Event`, and futex methods
- **[std.Thread](std-thread.md)** - Thread creation, joining, and thread-local facilities