Skip to content

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:

  • args is a tuple — .{arg1, arg2, ...} — passed verbatim to the function.
  • io.concurrent copies args before returning. Stack-allocated args are safe — no heap context needed.

  • No io is 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) injects error.Canceled at the worker's next cancellation point, then awaits.

  • Future is a resource — call await or cancel exactly 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!voidvoid, !void, and Cancelable!void all work.

  • error.Canceled returned by a worker is swallowed at the Group boundary — it's a cancellation propagation stop, not an error you see.

  • group.await(io) returns Cancelable!void.

  • group.cancel(io) injects error.Canceled into every running worker, then waits. Safe to call again after an await — a no-op.

  • group.concurrent after group.await starts 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, .{ mbh, null });
try sel.concurrent(.pool_ev, pool.getWaitResult, .{ ph, 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:

register
await
process
register again

Internals (verified from std/Io.zig):

  • queue: Queue(U) — completed results land here.
  • group: Group — owns every spawned concurrent task.
  • select.await() is queue.getOne(io) — blocks until the next result arrives.
  • select.concurrent(field, fn, args) spawns fn via group, wraps its return value as @unionInit(U, @tagName(field), result), and pushes it onto queue.

Direct push — no spawn, for a result that's already available or an external callback:

select.queue.putOneUncancelable(select.io, .{ .field = value }) catch {};

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:

  1. select.concurrent(field, blockingFn, args) — spawn a blocking function.
  2. 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 Cancelable in 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.