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
resumeFor ordinary programs, use one of these instead:
| Need | Use |
|---|---|
| Run work in parallel | std.Thread |
| Protect shared state | std.Thread.Mutex |
| Wait for state changes | std.Thread.Condition |
| Count or signal resources | std.Thread.Semaphore |
| Do one small shared operation | std.atomic |
| Build evented I/O | explicit 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.