पाठ 22 / 25
Threads, Atomics and Synchronisation
Write multithreaded code with std.Thread, mutexes and atomics.
Concurrency with OS threads
Zig's standard library provides std.Thread for OS threads: std.Thread.spawn(.{}, function, .{ args }) starts one and join waits for it. Shared state is protected with std.Thread.Mutex (lock and defer unlock), RwLock, Condition and Semaphore, while std.atomic.Value(T) offers lock-free atomic operations (load, store, fetchAdd, cmpxchgWeak) with explicit memory orderings (.monotonic, .acquire, .release, .seq_cst). std.Thread.Pool with a WaitGroup distributes tasks across worker threads. Zig does not prevent data races at compile time, so design ownership carefully: give each thread its own data where possible and share only through synchronised structures, and use thread-safe allocators (the DebugAllocator is thread-safe by default; arenas are not). Earlier Zig versions had language-level async/await, which was removed during the move to the self-hosted compiler; a new design for asynchronous I/O based on an Io interface passed explicitly (like allocators) is being developed in the standard library, so check the current release before relying on it. For event loops today, projects use OS threads, std.posix polling APIs or libraries such as libxev.
Threads sharing a counter
Workers update shared state through a mutex or atomic operations.
Parallel work with threads, a mutex and an atomic
Each worker sums a chunk; results are combined safely.
const std = @import("std");
const Shared = struct {
mutex: std.Thread.Mutex = .{},
total: u64 = 0,
processed: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
};
fn worker(shared: *Shared, chunk: []const u32) void {
var local: u64 = 0;
for (chunk) |v| local += v; // no sharing in the hot loop
shared.mutex.lock();
defer shared.mutex.unlock();
shared.total += local; // short critical section
_ = shared.processed.fetchAdd(chunk.len, .monotonic);
}
pub fn main() !void {
var data: [100_000]u32 = undefined;
for (&data, 0..) |*d, i| d.* = @intCast(i % 100);
var shared = Shared{};
var threads: [4]std.Thread = undefined;
const chunk_len = data.len / threads.len;
for (&threads, 0..) |*t, i| {
const start = i * chunk_len;
const end = if (i == threads.len - 1) data.len else start + chunk_len;
t.* = try std.Thread.spawn(.{}, worker, .{ &shared, data[start..end] });
}
for (threads) |t| t.join();
std.debug.print("total={d} processed={d}\n", .{ shared.total, shared.processed.load(.monotonic) });
// total = 4_950_000 (each block of 100 values sums to 4950, and there are 1_000 blocks)
}Accumulate locally, combine once
Locking a mutex for every element serialises the threads. Let each thread accumulate into a local variable and take the lock only once to merge its result.
त्वरित जाँच: What is the status of async/await in current Zig?
- It is stable and widely used
- It was always unsupported
- Language-level async was removed; a new explicit Io-based design is in development
- It is required for all I/O
Answer
Language-level async was removed; a new explicit Io-based design is in development — Async keywords were removed during the self-hosted compiler transition.