⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Threads

Built and run natively by CI. wasm32-wasi is single-threaded, so std.Thread.spawn does not compile for it at all.
const std = @import("std");

fn addTo(total: *u32, amount: u32) void {
    total.* += amount;
}

fn addLocked(total: *u32, mutex: *std.Io.Mutex, io: std.Io, amount: u32) void {
    mutex.lock(io) catch return;
    defer mutex.unlock(io);
    total.* += amount;
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    var buf: [256]u8 = undefined;
    var file_writer = std.Io.File.stdout().writerStreaming(io, &buf);
    const out = &file_writer.interface;

    // One thread, joined explicitly.
    var value: u32 = 0;
    const thread = try std.Thread.spawn(.{}, addTo, .{ &value, 42 });
    thread.join();
    try out.print("single: {d}\n", .{value});

    // Many threads touching one variable need synchronisation; without the
    // mutex this is a data race and the total is unreliable.
    var total: u32 = 0;
    var mutex: std.Io.Mutex = .init;
    var threads: [8]std.Thread = undefined;
    for (&threads) |*t| {
        t.* = try std.Thread.spawn(.{}, addLocked, .{ &total, &mutex, io, 1 });
    }
    for (threads) |t| t.join();
    try out.print("locked total: {d}\n", .{total});

    // Atomics cover the simple counter case without a lock.
    var counter = std.atomic.Value(u32).init(0);
    _ = counter.fetchAdd(1, .monotonic);
    _ = counter.fetchAdd(1, .monotonic);
    try out.print("atomic: {d}\n", .{counter.load(.monotonic)});

    try out.flush();
}

Spawn and join

const thread = try std.Thread.spawn(.{}, function, .{ args... });
thread.join();

The arguments are a tuple, matching the function’s parameters. join blocks until the thread finishes; detach gives up the handle instead.

Synchronisation moved to std.Io

This is the part that breaks older code. std.Thread.Mutex no longer exists. It is std.Io.Mutex now, and lock takes the io instance:

var mutex: std.Io.Mutex = .init;
try mutex.lock(io);
defer mutex.unlock(io);

lock can fail only because it can be cancelled, which is what makes it composable with the rest of the Io machinery.

Atomics for the simple cases

A counter does not need a mutex:

var counter = std.atomic.Value(u32).init(0);
_ = counter.fetchAdd(1, .monotonic);

Note the _ =: fetchAdd returns the previous value, and Zig will not let you drop it silently.

Prefer structured concurrency

For most work, Io.async and Io.Group are a better fit than spawning raw threads: they are cancellable, they cannot leak a running task past their scope, and they work on targets that have no threads at all, including the one this site runs on.