On this page
article
Async I/O
Module: core/io
Violet’s async model uses an explicit *Io parameter rather than async/await keywords. Functions that take *Io may suspend; functions without it are provably synchronous.
Definitions
interface Io {
fn waitFd(*self, fd: Fd, event: IoEvent): !void;
fn sleep(*self, dur: Duration): !void;
fn yield_(*self): void;
fn spawnBlocking(*self, alloc: Allocator, work: BoxedBlockingTask): !void;
fn scope(*self, alloc: Allocator, body: BoxedScopeTask): !void;
fn spawn(*self, alloc: Allocator, task: BoxedTask): !TaskHandle;
fn spawnDetached(*self, alloc: Allocator, task: BoxedTask): !void;
fn waker(*self): Waker;
fn handle(*self, alloc: Allocator): IoHandle;
fn run(*self): !void;
fn shutdown(*self): void;
}
interface IoScope: Io {
fn spawn(*self, alloc: Allocator, task: BoxedTask): !void;
}
Usage
Structured Concurrency (Recommended)
fn serveRequests(io: *Io, alloc: Allocator): !void {
using alloc;
try io.scope([](s: *IoScope) {
while true {
var conn = try acceptConnection(io);
try s.spawn([move conn](_: *IoScope) {
try handleConnection(io, conn);
});
}
});
-- all connection tasks joined before returning
}
Inside io.scope, use s for everything: IoScope extends Io, so s.waitFd, s.sleep, s.spawn all work through the scope.
Manual Task Management
-- spawn with handle (must join or detach)
var handle = try io.spawn(alloc, [](s: *IoScope) {
try doWork(io);
});
-- ... do other work ...
try handle.join(); -- wait for completion
-- or:
handle.detach(); -- convert to daemon task (cancelled on shutdown)
-- fire and forget daemon
try io.spawnDetached(alloc, [](s: *IoScope) {
while true {
try emitMetrics(io);
try s.sleep(Duration::seconds(30));
}
});
TaskHandle is linear — you must join, detach, or awaitCancelled. Dropping without one is a compile error (asserts in debug).
Cancellation
Tasks are cancelled cooperatively — error.Cancelled is returned at suspension points. errdefer handles cleanup:
fn worker(io: *Io, alloc: Allocator): !void {
var list = List[u8]::new();
errdefer list.deinit(); -- runs if cancelled or any other error
try io.waitFd(fd, .Read); -- returns error.Cancelled if cancelled
try process(&list);
list.deinit();
}
CPU-Bound Work
fn heavyWork(io: *Io, alloc: Allocator): !u64 {
return try io.spawnBlocking(alloc, [](_: void): u64 {
-- runs on thread pool, Violet task suspends
return computeResult();
});
}
Waker and Cross-Scheduler Communication
type Waker = struct {
ptr: *anyopaque,
vtable: *const WakerVTable,
};
-- Waker holds Weak reference to scheduler
-- wake() is a no-op if scheduler has been destroyed
#currentScheduler [TBD]
fn downloadEasy(url: []u8): ![]u8 {
var io = #currentScheduler(); -- thread-local escape hatch
var conn = try tcp.connect(io, url);
return try conn.readAll(alloc);
}
Use #currentScheduler only in application code, not library code. Libraries should take explicit *Io.