zig-skills/references/std-thread.md

200 lines
5.0 KiB
Markdown

# 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:
```zig
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:
```zig
const id = std.Thread.getCurrentId();
const cpu_count = std.Thread.getCpuCount() catch 1;
std.Thread.sleep(10 * std.time.ns_per_ms);
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` | `std.Io.Futex` |
Lock-free atomics do not need `std.Io`.
## Mutex
Use cancelable locking when cancelation should be honored:
```zig
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 for short critical sections where interruption would corrupt state or skip required cleanup:
```zig
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`.
```zig
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
```zig
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
```zig
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`.
```zig
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.
```zig
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)` for queue integrity and allocator metadata updates when cancellation cannot safely interrupt the critical section.
- 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?