Patterns — Futures, Select, Group, Cancellation
New to std.Io concepts? See Addendums — Io 101 first.
Future patterns
Direct Future
When to use.
- Only one asynchronous operation. No Select loop needed.
Code shape.
Future cancellation
When to use.
- Abort one asynchronous operation.
Code shape.
Why.
- The item stays in the mailbox/pool.
- Only the wait is canceled.
Io.Select patterns
Event loop — register, await, re-register
When to use.
- Wait on several sources at once: mailbox, pool, timer, external push.
Code shape.
var buf: [8]MasterEvent = undefined;
var sel: std.Io.Select(MasterEvent) = std.Io.Select(MasterEvent).init(io, &buf);
try sel.concurrent(.inbox, mailbox.receiveResult, .{ mbh, null });
try sel.concurrent(.pool_ev, pool.getWaitResult, .{ ph, TAG, null });
try sel.concurrent(.timer, sleepFn, .{ sleep_t, io });
while (true) {
const event: MasterEvent = try sel.await();
switch (event) {
.inbox => |r| switch (r) {
.item => |handle| {
// process, then re-register the source
try sel.concurrent(.inbox, mailbox.receiveResult, .{ mbh, null });
},
.closed, .canceled, .timeout => break,
},
// ...
}
}
Why.
- Each registration produces exactly one completion.
- Re-register the source after each item.
The rhythm.
Example: examples/layer4/031-select_graceful_shutdown.zig, examples/layer4/028-select_mixed_sources.zig.
Mailbox as event source
When to use.
- Event-driven Master.
Code shape.
Pool as event source
When to use.
- Wait for reusable items.
Code shape.
Mixed event sources
When to use.
- One loop coordinates everything.
Pattern.
Example: examples/layer4/028-select_mixed_sources.zig.
Backpressure via getWaitResult in Select
When to use.
- A producer must slow down when no buffers are free.
- Pool availability becomes an event source in the same loop as data.
Code shape.
try sel.concurrent(.buf_ev, pool.getWaitResult, .{ buf_ph, VideoBufferPolyHelper.TAG, null });
// ...
const ev = try sel.await();
switch (ev) {
.buf_ev => |r| switch (r) {
.item => |handle| {
// fill buffer, route it, then re-register for the next free buffer
try sel.concurrent(.buf_ev, pool.getWaitResult, .{ buf_ph, VideoBufferPolyHelper.TAG, null });
},
.closed, .canceled, .timeout, .not_created => break,
},
}
- The loop blocks until a worker returns a buffer.
- No sleep. No poll. The pool wakes the waiter.
Example: stories/video_transcoder/video_transcoder.zig.
Direct push — putOneUncancelable
When to use.
- A result is already available, or an external thread or callback must inject one without spawning.
Code shape.
Example: examples/layer4/043-select_direct_push.zig.
Graceful cancel walk — recover in-flight items
When to use.
- Shutting down a Select loop. Spawned sources may still hold items. None must leak.
Code shape.
while (sel.cancel()) |event| {
switch (event) {
.inbox => |r| switch (r) {
.item => |handle| {
var slot: Slot = handle;
helpers.freeSlot(&slot, allocator); // recover the item
},
.canceled, .closed, .timeout => {},
},
.pool_ev => |r| switch (r) {
.item => |handle| {
var slot: Slot = handle;
pool.put(ph, &slot); // recycle it
},
.canceled, .closed, .timeout, .not_created => {},
},
.timer => {},
}
}
Example: examples/layer4/031-select_graceful_shutdown.zig.
cancelDiscard — timer-only or no-item sources
When to use.
- The remaining spawned sources carry no owned item (e.g. a timer). Discard them.
Code shape.
Example: stories/video_transcoder/video_transcoder.zig.
Io.Group patterns
Worker set — concurrent then await
When to use.
- Run several workers. Wait for all to finish.
- Spawn now, await later — the spawn does not block.
Code shape.
var group: Io.Group = .init;
try group.concurrent(io, workerFn, .{&ctx0});
try group.concurrent(io, workerFn, .{&ctx1});
try group.await(io);
- Worker return type must coerce to
Cancelable!void.
Example: stories/video_transcoder/video_transcoder.zig.
Reusable Group
When to use.
- Multiple execution rounds.
Pattern.
Why.
- Group may be reused after completion.
Shutdown signal — close the source mailbox
When to use.
- Stop a Group of workers that block on
mailbox.receive.
Code shape.
// workers exit when receive returns error.Closed
var rem: std.DoublyLinkedList = mailbox.close(ready_queue);
// walk rem, recover any unreceived items
try group.await(io);
- Close is the end-of-stream signal. Workers return on
error.Closed.
Example: stories/video_transcoder/video_transcoder.zig.
Shutdown signal — group.cancel
When to use.
- Stop a Group of workers that block on
pool.get_wait, with no mailbox to close.
Code shape.
- Blocked workers return
error.Canceled. A worker that already finished is unaffected.
Example: examples/layer4/059-mailbox_less_pool_group_workers.zig.
Cancellation patterns
Cancellation boundary
When to use.
- Designing APIs.
Rule.
Only waiting operations are cancelable.
Examples.
- mailbox.receive
- pool.get_wait
- receiveResult
- getWaitResult
Everything else completes normally.
Cancellation keeps items in place
When to use.
- Recovering after cancellation.
Pattern.
Close versus Cancel
Pattern.
Never substitute one for the other.
Error handling on receive
When to use.
- A worker blocks on
mailbox.receiveorpool.get_waitand must react to each outcome.
Code shape.
mailbox.receive(ctx.mbh, &slot, null) catch |err| switch (err) {
error.Canceled => return error.Canceled, // report up — Master decides
error.Closed, error.Timeout => return, // end-of-stream — exit cleanly
error.Wakeup => continue, // poke — re-check loop condition
};
The distinction.
error.Canceled— external stop signal. Propagate it. Do not close anything.error.Closed— the Master closed the source. End of stream. Exit.error.Timeout— the wait window passed. Treat per domain.error.Wakeup— awakeUpAllpoke. No item. Re-check state and continue.- Never remap
error.Canceledtoerror.Closed. They mean different things.
Example: stories/video_transcoder/video_transcoder.zig, examples/layer4/059-mailbox_less_pool_group_workers.zig.