# Threads, Atomics and Synchronisation — Zig

Source: https://www.geekswithgeeks.com/en/zig/p-threads

> 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.

![Four worker icons with arrows to a central counter protected by a padlock, and a separate atomic counter icon with a lightning bolt.](assets/figures/zig/section-8-map.svg) — Figure 8.1 — Mutex-protected and atomic shared state.

## Parallel work with threads, a mutex and an atomic

Each worker sums a chunk; results are combined safely.

```zig
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.

**Quiz:** What is the status of async/await in current Zig?

- [ ] It is stable and widely used
- [ ] It was always unsupported
- [x] 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.
