202 lines
5.1 KiB
Markdown
202 lines
5.1 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.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:
|
|
|
|
```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 when waiting to acquire the lock must not be a
|
|
cancelation point. Cancelation does not interrupt an already-entered critical
|
|
section:
|
|
|
|
```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)` 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?
|