The Io Scheduler

Functions that may suspend take an explicit *Io parameter. This makes suspension visible at every call site — critical for lock safety:

  const io_mod = import("std/async");
const net    = import("std/net");

-- provably synchronous: no *Io parameter
fn computeHash(data: []const u8): u64 {
    return fnv1a(data);
}

-- may suspend: takes *Io
fn fetchData(io: *Io, url: []const u8, alloc: Allocator): ![]u8 {
    var conn = try net.TcpStream::connect(io, url);
    defer conn.deinit();
    return try conn.readAll(io, alloc);
}
  

Structured Concurrency

The safest way to run concurrent tasks — all tasks are joined when the scope exits:

  fn serve(io: *Io, alloc: Allocator): !void {
    using alloc;

    var listener = try net.TcpListener::bind(io, "0.0.0.0:8080");
    defer listener.deinit();

    -- structured: all connections joined before serve() returns
    try io.scope([](s: *IoScope) {
        while true {
            var conn = try listener.accept(io);
            try s.spawn([move conn](_: *IoScope) {
                handleConn(io, conn) catch |e| {
                    debug.print("error: {}\n", .{e});
                };
            });
        }
    });
}

fn handleConn(io: *Io, conn: net.TcpStream): !void {
    defer conn.deinit();

    var buf: [4096]u8 = #undefined;
    var n = try conn.read(io, &buf);

    -- echo back
    try conn.write(io, buf[0..n]);
}
  

Manual Task Management

When you need to track specific tasks:

  fn runPipeline(io: *Io, alloc: Allocator): !Report {
    using alloc;

    var ch = try Channel[WorkItem]::new(alloc, 32);
    defer ch.deinit(alloc);

    -- spawn producer
    var producer = try io.spawn([move ch.sender()](_: *IoScope) {
        for i in 0..100 {
            try ch.send(WorkItem { .id = i });
        }
        ch.close();
    });
    errdefer producer.cancel();

    -- spawn consumer, get result
    var consumer = try io.spawn([move ch.receiver()](_: *IoScope): !Report {
        var report = Report::new();
        while ch.recv() -> item {
            try report.process(item);
        }
        return report;
    });
    errdefer consumer.cancel();

    try producer.join(io);
    return try consumer.join(io);
}
  

Shared State With Mutex

  const sync = import("std/sync");

type Counter = struct {
    value: sync.Mutex[i32],
};

impl Counter {
    static fn new(): Self {
        return .{ .value = sync.Mutex[i32]::new(0) };
    }

    fn increment(*self): void {
        var guard = self.value.lock();
        guard.*.* += 1;
    }

    fn get(*const self): i32 {
        var guard = self.value.lock();
        return guard.*.*;
    }
}

fn main(ctx: process.Context): !void {
    var counter = Arc[Counter]::new(ctx.alloc, Counter::new());
    defer counter.deinit(ctx.alloc);

    try ctx.io.scope([](s: *IoScope) {
        for _ in 0..10 {
            var c = counter.clone(ctx.alloc);
            try s.spawn([move c](_: *IoScope) {
                for _ in 0..100 {
                    c.*.increment();
                }
                c.deinit(ctx.alloc);
            });
        }
    });

    debug.print("final: {}\n", .{counter.*.get()});   -- 1000
}
  

CPU-Bound Work

Don’t block the scheduler thread:

  fn processImage(io: *Io, alloc: Allocator, img: Image): !Image {
    -- offload heavy work to thread pool
    -- Violet task suspends, thread pool thread runs the closure
    return try io.spawnBlocking(alloc, [move img](_: void): !Image {
        return applyFilters(img);   -- CPU-intensive, runs on thread pool
    });
}