Skip to content

Async Status in Zig 0.16

Zig once had async functions as a language feature.

Zig once had async functions as a language feature.

In Zig 0.16, async function syntax is not part of the stable language model. Code should not be built around old examples that use:

async
await
suspend
resume

For ordinary programs, use one of these instead:

NeedUse
Run work in parallelstd.Thread
Protect shared statestd.Thread.Mutex
Wait for state changesstd.Thread.Condition
Count or signal resourcesstd.Thread.Semaphore
Do one small shared operationstd.atomic
Build evented I/Oexplicit event loop or library design

This matters because old Zig examples may still appear online. They may describe async frames, suspension points, or await. Such examples belong to older Zig versions and should not be copied into Zig 0.16 code.

A simple concurrent Zig program today is usually written with threads:

const std = @import("std");

fn worker(id: u32) void {
    std.debug.print("worker {d}\n", .{id});
}

pub fn main() !void {
    const a = try std.Thread.spawn(.{}, worker, .{1});
    const b = try std.Thread.spawn(.{}, worker, .{2});

    a.join();
    b.join();
}

This creates two operating system threads. Each runs worker.

For waiting on an event, use synchronization:

const std = @import("std");

const State = struct {
    mutex: std.Thread.Mutex = .{},
    condition: std.Thread.Condition = .{},
    ready: bool = false,
};

fn producer(state: *State) void {
    state.mutex.lock();
    defer state.mutex.unlock();

    state.ready = true;
    state.condition.signal();
}

fn consumer(state: *State) void {
    state.mutex.lock();
    defer state.mutex.unlock();

    while (!state.ready) {
        state.condition.wait(&state.mutex);
    }

    std.debug.print("ready\n", .{});
}

pub fn main() !void {
    var state = State{};

    const a = try std.Thread.spawn(.{}, consumer, .{&state});
    const b = try std.Thread.spawn(.{}, producer, .{&state});

    a.join();
    b.join();
}

This is explicit. The state is visible. The lock is visible. The wait is visible.

That is the current Zig style.

Async I/O is still possible as a library design. It is not magic syntax. A library may use nonblocking file descriptors, OS event systems, callbacks, queues, or worker threads.

On Linux, this may involve epoll or io_uring.

On BSD and macOS, it may involve kqueue.

On Windows, it may involve IOCP.

Zig does not require one model for all programs. A program can choose the model that fits the problem.

This keeps the language small.

For many tools, blocking I/O is enough.

For example, a file copy program can read and write in a loop:

const std = @import("std");

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("copy would go here\n", .{});
    try stdout.flush();
}

For a server handling many connections, one thread per connection may be too expensive. Then the program needs an event loop, a thread pool, or both.

The important point is that Zig 0.16 does not hide this choice behind async syntax.

You choose the concurrency model directly.

Exercise 18-17. Find an old Zig async example and identify the syntax that no longer belongs in Zig 0.16 code.

Exercise 18-18. Rewrite a simple async style example using std.Thread.spawn.

Exercise 18-19. Write a program that uses a condition variable instead of a busy loop.

Exercise 18-20. Write down which model you would use for a command-line tool, a file server, and a CPU-bound worker program.