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.Poolwas removed.std.Thread.Mutex.Recursivewas removed.std.oncewas removed.- Blocking synchronization in I/O-aware code should use
std.Ioprimitives.
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.Canceledby 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.Iotask 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.Poolreplaced with a design that preserves ordering and synchronization semantics? - Are atomics used only for lock-free state where blocking is not required?