Io 101
If you are going to use std.Io directly - this page for you.
(BTW - API Reference and Patterns pages assume your Io knowledge)
The four things std.Io gives you
Io access to the runtime — passed to anything that waits or spawns
Future(T) a result that isn't ready yet
Io.Select waits on several event sources at once
Io.Group runs several tasks, waits for all of them
-
Io— think of it as a capability token. Anything that needs threads, timers, or waiting takes one. -
Future(T)— you get the value by calling.await(). -
Io.Select— internally:queue: Queue(U)+group: Group.select.await()reads the next completed result from the queue. -
Io.Group— runs several tasks concurrently, waits for all of them, or cancels all of them.
Future — one asynchronous result
var fut = try io.concurrent(workerFn, .{&ctx});
// ... do other work ...
try fut.await(io); // blocks until worker exits; returns worker's return type
concurrent() ──► Future(T)
│
┌───────┼───────┐
V V
.await() .cancel(io)
│ │
V V
result error.Canceled
Rules:
argsis a tuple —.{arg1, arg2, ...}— passed verbatim to the function.-
io.concurrentcopiesargsbefore returning. Stack-allocated args are safe — no heap context needed. -
No
iois injected automatically. If the worker needs it, pass it explicitly:.{io, &ctx}. -
fut.await(io)returns the worker's return type directly. -
fut.cancel(io)injectserror.Canceledat the worker's next cancellation point, then awaits. -
Futureis a resource — callawaitorcancelexactly once.
Io.Group — several tasks, one wait
var group: std.Io.Group = .init;
defer group.cancel(io); // safe: no-op if already awaited
try group.concurrent(io, workerFn, .{&ctx1});
try group.concurrent(io, workerFn, .{&ctx2});
try group.concurrent(io, workerFn, .{&ctx3});
try group.await(io); // blocks until all workers exit
Rules:
-
Worker return type must be coercible to
Cancelable!void—void,!void, andCancelable!voidall work. -
error.Canceledreturned by a worker is swallowed at the Group boundary — it's a cancellation propagation stop, not an error you see. -
group.await(io)returnsCancelable!void. -
group.cancel(io)injectserror.Canceledinto every running worker, then waits. Safe to call again after anawait— a no-op. -
group.concurrentaftergroup.awaitstarts a new round in the same Group.
Io.Select — several event sources, one loop
var buf: [8]MasterEvent = undefined;
var sel: std.Io.Select(MasterEvent) = std.Io.Select(MasterEvent).init(io, &buf);
try sel.concurrent(.inbox, mailbox.receiveResult, .{ mbx, null });
try sel.concurrent(.pool_ev, pool.getWaitResult, .{ pl, TAG, null });
while (true) {
const event = try sel.await();
switch (event) {
.inbox => |r| { /* process, then re-register */ },
.pool_ev => |r| { /* process, then re-register */ },
}
}
The rhythm:
Internals (verified from std/Io.zig):
queue: Queue(U)— completed results land here.group: Group— owns every spawned concurrent task.select.await()isqueue.getOne(io)— blocks until the next result arrives.select.concurrent(field, fn, args)spawnsfnviagroup, wraps its return value as@unionInit(U, @tagName(field), result), and pushes it ontoqueue.
Direct push — no spawn, for a result that's already available or an external callback:
A return value can be thrown away:
-
The wrapper puts the task's return value with
putOneUncancelable(...) catch error.Closed => {}. -
A closed queue means that value is discarded. Silently. No log, no error.
- The task has already returned by then. It cannot react even in principle.
cancelDiscardcloses the queue before cancelling tasks, so this is its normal path.cancelcloses it after, so every task's value lands first.
Consequence: a task that returns an allocated resource must be shut down with
cancel, not cancelDiscard. std.Io says as much on cancelDiscard itself.
Event sources — the pattern behind Select
An event source is a blocking function passed to select.concurrent. When it returns,
the result lands in the queue.
blockingFn ────► select.concurrent(field, blockingFn, args)
│
V (fn runs, result → queue)
Io.Select.queue
│ │
V V
completed canceled
(result) (error.Canceled)
Two ways to produce an event:
select.concurrent(field, blockingFn, args)— spawn a blocking function.select.queue.putOneUncancelable(io, value)— push directly, no spawn.
Matryoshka's Mailbox and Pool plug into exactly this pattern — see
API Reference — Mailbox event source helpers and
API Reference — Pool event source helpers.
Cancellation
A function that waits — for data, for a timeout, for a condition — can be canceled by
the runtime.
-
If a function can be canceled, its return type includes
Cancelablein the error union. The signature is the only source of truth for this. -
Cancel is something you do to a Future or a Group — it never happens on its own.
concurrent() ──► Future(T)
│
┌───────┼───────┐
V V
.await() .cancel(io)
│ │
V V
result error.Canceled
Back to: Patterns — Futures, Select, Group, Cancellation for how
Matryoshka code actually uses these primitives.