zig-skills/references/std-thread.md

5.1 KiB

std.Thread and std.Io Synchronization (Zig 0.16.0)

Primary release-note source: https://ziglang.org/download/0.16.0/release-notes.html

Zig 0.16 keeps std.Thread for OS thread spawning and thread utilities, but blocking synchronization and task concurrency should move to std.Io when it may block inside code that participates in the I/O runtime.

What Remains in std.Thread

Use std.Thread for explicit OS threads:

const std = @import("std");

fn worker(id: usize) void {
    std.debug.print("worker {d}\n", .{id});
}

pub fn main() !void {
    const thread = try std.Thread.spawn(.{}, worker, .{42});
    thread.join();
}

Useful thread utilities:

const id = std.Thread.getCurrentId();
const cpu_count = std.Thread.getCpuCount() catch 1;
std.Thread.yield() catch {};

Removed or Avoided std.Thread APIs

  • std.Thread.Pool was removed.
  • std.Thread.Mutex.Recursive was removed.
  • std.once was removed.
  • Blocking synchronization in I/O-aware code should use std.Io primitives.

std.Io Sync Migration

Release-note migration map:

Old New
std.Thread.Mutex std.Io.Mutex
std.Thread.Condition std.Io.Condition
std.Thread.Semaphore std.Io.Semaphore
std.Thread.RwLock std.Io.RwLock
std.Thread.ResetEvent std.Io.Event
std.Thread.WaitGroup std.Io.Group
std.Thread.Futex operations io.futexWait / io.futexWaitTimeout / io.futexWaitUncancelable / io.futexWake

Lock-free atomics do not need std.Io.

Mutex

Use cancelable locking when cancelation should be honored:

var mutex: std.Io.Mutex = .init;
var value: u64 = 0;

fn increment(io: std.Io) !void {
    try mutex.lock(io);
    defer mutex.unlock(io);
    value += 1;
}

Use uncancelable locking when waiting to acquire the lock must not be a cancelation point. Cancelation does not interrupt an already-entered critical section:

mutex.lockUncancelable(io);
defer mutex.unlock(io);

Do not replace mutexes with spin loops unless there is a documented, measured special case.

Condition

std.Io.Condition waits with an io and a std.Io.Mutex.

var mutex: std.Io.Mutex = .init;
var condition: std.Io.Condition = .init;
var ready = false;

fn waitUntilReady(io: std.Io) !void {
    try mutex.lock(io);
    defer mutex.unlock(io);

    while (!ready) {
        try condition.wait(io, &mutex);
    }
}

fn setReady(io: std.Io) void {
    mutex.lockUncancelable(io);
    defer mutex.unlock(io);

    ready = true;
    condition.broadcast(io);
}

Semaphore

var semaphore: std.Io.Semaphore = .{ .permits = 3 };

fn usePermit(io: std.Io) !void {
    try semaphore.wait(io);
    defer semaphore.post(io);
    // protected limited-concurrency work
}

Use waitUncancelable(io) only when a cancellation point would be invalid.

RwLock

var rw: std.Io.RwLock = .init;

fn read(io: std.Io) !void {
    try rw.lockShared(io);
    defer rw.unlockShared(io);
}

fn write(io: std.Io) !void {
    try rw.lock(io);
    defer rw.unlock(io);
}

Event

std.Thread.ResetEvent maps to std.Io.Event.

var event: std.Io.Event = .unset;

fn waiter(io: std.Io) !void {
    try event.wait(io);
}

fn signal(io: std.Io) void {
    event.set(io);
}

Group Instead of WaitGroup / Thread.Pool

Use std.Io.Group to spawn and await related tasks.

fn doWork(io: std.Io) !void {
    var group: std.Io.Group = .init;
    errdefer group.cancel(io);

    group.async(io, worker, .{ io, 0 });
    group.async(io, worker, .{ io, 1 });

    try group.await(io);
}

fn worker(io: std.Io, id: usize) void {
    _ = .{ io, id };
}

Do not mechanically replace std.Thread.Pool with std.Io.Group if tasks synchronously depend on the caller or mutate shared state without I/O-aware synchronization. Re-evaluate ownership and synchronization first.

Cancelation

Cancelable waits/locks may return error.Canceled.

Rules:

  • Propagate error.Canceled by default.
  • Only ignore it in code that requested cancelation.
  • If you catch it and continue, use io.recancel() when the request should remain active.
  • Use uncancelable waits/locks sparingly and only where cancellation would violate invariants.

Application Guidance

  • Queues, allocators, registries, timers, and worker systems that use blocking sync should accept/store std.Io.
  • Use lockUncancelable(io) only when waiting for queue or allocator locks must not observe cancelation; keep the acquired critical section short.
  • Keep OS threads for code that is explicitly thread-owned, such as dedicated worker threads or external API callbacks.
  • Prefer an application's existing scheduler for application work unless the standard std.Io task API is explicitly the better fit.

Review Checklist

  • Is this code in an I/O-aware path? If yes, sync primitives should be std.Io.*.
  • Does every blocking lock/wait receive an io?
  • Is cancelation either propagated or deliberately protected?
  • Was std.Thread.Pool replaced with a design that preserves ordering and synchronization semantics?
  • Are atomics used only for lock-free state where blocking is not required?