On this page
article
Chapter 13: Concurrency
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
});
}