Concurrency: Threads, Atomics and Synchronization

32.1 Threads

std.Thread.spawn(config, func, args_tuple). Config options include .stack_size and .allocator.

t.detach(), std.Thread.getCurrentId(), std.Thread.getCpuCount(), std.Thread.sleep(ns), threadlocal var.

fn worker(id: usize, counter: *std.atomic.Value(u64)) void {
    for (0..1000) |_| _ = counter.fetchAdd(1, .monotonic);
    std.debug.print("worker {d} done\n", .{id});
}

pub fn main() !void {
    var counter = std.atomic.Value(u64).init(0);
    var threads: [4]std.Thread = undefined;
    for (&threads, 0..) |*t, i| {
        t.* = try std.Thread.spawn(.{}, worker, .{ i, &counter });
    }
    for (threads) |t| t.join();
    std.debug.print("total = {d}\n", .{counter.load(.monotonic)});
}

32.2 Mutex, RwLock, Condition, Semaphore

const SharedQueue = struct {
    mutex: std.Thread.Mutex = .{},
    cond: std.Thread.Condition = .{},
    items: std.ArrayList(u32) = .empty,

    fn push(self: *SharedQueue, allocator: std.mem.Allocator, v: u32) !void {
        self.mutex.lock();
        defer self.mutex.unlock();
        try self.items.append(allocator, v);
        self.cond.signal();
    }

    fn pop(self: *SharedQueue) u32 {
        self.mutex.lock();
        defer self.mutex.unlock();
        while (self.items.items.len == 0) self.cond.wait(&self.mutex);
        return self.items.orderedRemove(0);
    }
};
// Also: std.Thread.RwLock, std.Thread.Semaphore, std.Thread.ResetEvent, std.Thread.WaitGroup, std.once

32.3 Atomics

Memory orderings: .unordered, .monotonic, .acquire, .release, .acq_rel, .seq_cst.

var flag = std.atomic.Value(bool).init(false);
flag.store(true, .release);
if (flag.load(.acquire)) {}
var n = std.atomic.Value(u32).init(0);
_ = n.fetchAdd(1, .seq_cst);
_ = n.cmpxchgStrong(1, 2, .acq_rel, .monotonic);   // returns null on success
std.atomic.spinLoopHint();

32.4 Thread Pool

var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = 8 });
defer pool.deinit();
var wg: std.Thread.WaitGroup = .{};
for (chunks) |chunk| pool.spawnWg(&wg, processChunk, .{chunk});
pool.waitAndWork(&wg);

32.5 Concurrency Best Practices

Prefer data partitioning (each thread owns its slice) over shared mutable state.

The allocator must be thread-safe if shared (smp_allocator, DebugAllocator with .thread_safe = true (default), or a per-thread arena).

Use ThreadSanitizer (-fsanitize-thread) to find data races.