Skip to main content

Threading API

The std.Thread module provides kernel thread management and concurrency primitives. It acts as a namespace for concurrency primitives that operate on kernel threads. Note: For concurrency primitives that interact with the I/O interface, see std.Io.

Thread Management

spawn

pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread
Spawns a new thread which executes function using args.
config
SpawnConfig
required
Configuration for thread spawning
function
anytype
required
Function to execute (must return void, !void, u8, !u8, or noreturn)
args
anytype
required
Arguments tuple to pass to the function
return
Thread
Thread handle - must call join() or detach()
Example:
fn worker(name: []const u8, count: u32) void {
    for (0..count) |i| {
        std.debug.print("{s}: {d}\n", .{ name, i });
    }
}

const thread = try std.Thread.spawn(.{}, worker, .{ "Thread-1", 10 });
thread.join();

SpawnConfig

pub const SpawnConfig = struct {
    stack_size: usize = default_stack_size,
    allocator: ?std.mem.Allocator = null,

    pub const default_stack_size = 16 * 1024 * 1024; // 16 MB
};
stack_size
usize
default:"16 MB"
Size in bytes of the thread’s stack
allocator
?Allocator
default:"null"
Allocator for thread memory (required on WASI)
Example:
const thread = try std.Thread.spawn(.{
    .stack_size = 1024 * 1024, // 1 MB stack
    .allocator = allocator,
}, worker, .{});

join

pub fn join(self: Thread) void
Waits for the thread to complete, then deallocates resources. Warning: Once called, the Thread object is consumed and cannot be used again. Example:
const thread = try std.Thread.spawn(.{}, worker, .{});
thread.join(); // Wait for completion

detach

pub fn detach(self: Thread) void
Releases the obligation to call join() and allows the thread to clean up its own resources. Warning: Once called, the Thread object is consumed and cannot be used again. Example:
const thread = try std.Thread.spawn(.{}, worker, .{});
thread.detach(); // Fire and forget

getHandle

pub fn getHandle(self: Thread) Handle
Returns the platform-specific thread handle.
return
Handle
  • Windows: windows.HANDLE
  • POSIX: pthread_t
  • Linux: i32 (TID)
  • WASI: i32

Thread Identity

Id

pub const Id = switch (native_os) {
    .linux, .dragonfly, .netbsd, .freebsd, .openbsd, .haiku, .wasi, .serenity => u32,
    .macos, .ios, .tvos, .watchos, .visionos => u64,
    .windows => windows.DWORD,
    else => usize,
};
Represents a thread ID unique within a process.

getCurrentId

pub fn getCurrentId() Id
Returns the platform ID of the caller’s thread. Attempts to use thread locals and avoid syscalls when possible. Example:
const tid = std.Thread.getCurrentId();
std.debug.print("Thread ID: {}\n", .{tid});

Thread Naming

setName

pub fn setName(self: Thread, name: []const u8) SetNameError!void
Sets the thread’s name for debugging purposes.
name
[]const u8
required
Thread name (max length: max_name_len)
Platform Support:
  • ✅ Linux, Windows, macOS, FreeBSD, NetBSD, OpenBSD, DragonFly, illumos, Serenity
  • ❌ Most other platforms
Example:
const thread = try std.Thread.spawn(.{}, worker, .{});
try thread.setName("worker-1");
thread.join();

getName

pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]const u8
Gets the thread’s name.
buffer_ptr
*[max_name_len:0]u8
required
Buffer to store the name
return
?[]const u8
Thread name if set, null if not set
  • Windows: WTF-8 encoded
  • Other: Opaque byte sequence
Example:
var buf: [std.Thread.max_name_len:0]u8 = undefined;
if (try thread.getName(&buf)) |name| {
    std.debug.print("Thread name: {s}\n", .{name});
}

max_name_len

pub const max_name_len: usize
Platform-specific maximum thread name length:
  • Linux: 15
  • Windows: 31
  • macOS/iOS: 63
  • NetBSD: 31
  • FreeBSD: 15
  • OpenBSD: 23
  • DragonFly: 1023
  • illumos: 31
  • Serenity: 63

CPU Information

getCpuCount

pub fn getCpuCount() CpuCountError!usize
Returns the platform’s view on the number of logical CPU cores available.
return
usize
Number of CPUs (guaranteed to be >= 1)
Example:
const cpu_count = try std.Thread.getCpuCount();
std.debug.print("CPUs: {}\n", .{cpu_count});

// Spawn one thread per CPU
for (0..cpu_count) |i| {
    _ = try std.Thread.spawn(.{}, worker, .{i});
}

Thread Yielding

yield

pub fn yield() YieldError!void
Yields the current thread, potentially allowing other threads to run. Example:
while (working) {
    if (try processItem()) {
        // Successfully processed item
    } else {
        // No work available, yield to other threads
        try std.Thread.yield();
    }
}

Synchronization Primitives

Mutex

pub const Mutex = @import("Thread/Mutex.zig");
Mutual exclusion lock. Example:
var mutex = std.Thread.Mutex{};
var counter: u32 = 0;

fn increment() void {
    mutex.lock();
    defer mutex.unlock();
    counter += 1;
}

RwLock

pub const RwLock = @import("Thread/RwLock.zig");
Reader-writer lock allowing multiple readers or one writer. Example:
var rwlock = std.Thread.RwLock{};
var data: [100]u8 = undefined;

fn reader() void {
    rwlock.lockShared();
    defer rwlock.unlockShared();
    // Multiple readers can access concurrently
    _ = data[0];
}

fn writer() void {
    rwlock.lock();
    defer rwlock.unlock();
    // Exclusive access for writing
    data[0] = 42;
}

Semaphore

pub const Semaphore = @import("Thread/Semaphore.zig");
Counting semaphore.

Condition

pub const Condition = @import("Thread/Condition.zig");
Condition variable for thread synchronization. Example:
var mutex = std.Thread.Mutex{};
var cond = std.Thread.Condition{};
var ready = false;

fn waiter() void {
    mutex.lock();
    defer mutex.unlock();
    
    while (!ready) {
        cond.wait(&mutex);
    }
    // Proceed when ready
}

fn signaler() void {
    mutex.lock();
    ready = true;
    mutex.unlock();
    
    cond.signal(); // Wake one waiter
}

Futex

pub const Futex = @import("Thread/Futex.zig");
Low-level futex operations for implementing synchronization primitives.

ResetEvent

pub const ResetEvent = enum(u32) {
    unset = 0,
    waiting = 1,
    is_set = 2,
};
A thread-safe logical boolean that can be set and unset, with blocking wait support.

ResetEvent.isSet

pub fn isSet(re: *const ResetEvent) bool
Checks if the event is set (non-blocking).

ResetEvent.wait

pub fn wait(re: *ResetEvent) void
Blocks until the event is set. Example:
var event = std.Thread.ResetEvent.unset;

fn worker() void {
    event.wait(); // Block until set
    // Proceed...
}

fn coordinator() void {
    // Do setup...
    event.set(); // Wake waiting threads
}

ResetEvent.timedWait

pub fn timedWait(re: *ResetEvent, timeout_ns: u64) error{Timeout}!void
Waits with a timeout.
timeout_ns
u64
required
Timeout in nanoseconds
Example:
event.timedWait(5 * std.time.ns_per_s) catch |err| switch (err) {
    error.Timeout => {
        std.debug.print("Timed out waiting\n", .{});
    },
};

ResetEvent.set

pub fn set(re: *ResetEvent) void
Sets the event, unblocking all waiting threads.

ResetEvent.reset

pub fn reset(re: *ResetEvent) void
Resets the event to unset state. Warning: Assumes no threads are blocked in wait.

Thread Pool

Pool

pub const Pool = @import("Thread/Pool.zig");
Thread pool for managing a collection of worker threads.

WaitGroup

pub const WaitGroup = @import("Thread/WaitGroup.zig");
Wait group for coordinating multiple concurrent operations.

Error Sets

SpawnError

pub const SpawnError = error{
    ThreadQuotaExceeded,
    SystemResources,
    OutOfMemory,
    LockedMemoryLimitExceeded,
    Unexpected,
};
ThreadQuotaExceeded
error
System-imposed thread limit reached (RLIMIT_NPROC, /proc/sys/kernel/threads-max, etc.)
SystemResources
error
Kernel cannot allocate sufficient memory
LockedMemoryLimitExceeded
error
mlockall is enabled and memory limit would be exceeded

YieldError

pub const YieldError = error{
    SystemCannotYield,
};

CpuCountError

pub const CpuCountError = error{
    PermissionDenied,
    SystemResources,
    Unsupported,
    Unexpected,
};

Usage Patterns

Worker Pool Pattern

const std = @import("std");
const Thread = std.Thread;

const WorkQueue = struct {
    mutex: Thread.Mutex = .{},
    items: std.ArrayList(WorkItem),
    
    fn pop(self: *WorkQueue) ?WorkItem {
        self.mutex.lock();
        defer self.mutex.unlock();
        return self.items.popOrNull();
    }
};

fn worker(queue: *WorkQueue) void {
    while (queue.pop()) |item| {
        processWork(item);
    }
}

pub fn main() !void {
    const cpu_count = try Thread.getCpuCount();
    var threads = try allocator.alloc(Thread, cpu_count);
    defer allocator.free(threads);
    
    var queue = WorkQueue{ .items = std.ArrayList(WorkItem).init(allocator) };
    defer queue.items.deinit();
    
    // Spawn worker threads
    for (threads) |*thread| {
        thread.* = try Thread.spawn(.{}, worker, .{&queue});
    }
    
    // Join all threads
    for (threads) |thread| {
        thread.join();
    }
}

Producer-Consumer Pattern

const Queue = struct {
    mutex: Thread.Mutex = .{},
    cond: Thread.Condition = .{},
    items: std.ArrayList(Item),
    done: bool = false,
};

fn producer(queue: *Queue) void {
    for (0..100) |i| {
        queue.mutex.lock();
        defer queue.mutex.unlock();
        
        queue.items.append(makeItem(i)) catch unreachable;
        queue.cond.signal();
    }
    
    queue.mutex.lock();
    queue.done = true;
    queue.mutex.unlock();
    queue.cond.broadcast();
}

fn consumer(queue: *Queue) void {
    while (true) {
        queue.mutex.lock();
        defer queue.mutex.unlock();
        
        while (queue.items.items.len == 0 and !queue.done) {
            queue.cond.wait(&queue.mutex);
        }
        
        if (queue.items.popOrNull()) |item| {
            queue.mutex.unlock();
            processItem(item);
            queue.mutex.lock();
        } else if (queue.done) {
            break;
        }
    }
}

Parallel Processing

pub fn parallelProcess(items: []Item) !void {
    const cpu_count = try Thread.getCpuCount();
    const threads = try allocator.alloc(Thread, cpu_count);
    defer allocator.free(threads);
    
    const chunk_size = (items.len + cpu_count - 1) / cpu_count;
    
    for (threads, 0..) |*thread, i| {
        const start = i * chunk_size;
        const end = @min(start + chunk_size, items.len);
        const chunk = items[start..end];
        
        thread.* = try Thread.spawn(.{}, processChunk, .{chunk});
    }
    
    for (threads) |thread| {
        thread.join();
    }
}

Best Practices

  1. Always join or detach: Failing to call join() or detach() will leak resources.
  2. Use appropriate stack sizes: Default 16MB may be too large for many threads. Adjust based on actual needs.
  3. Match thread count to workload: Use getCpuCount() for CPU-bound tasks, but consider more threads for I/O-bound work.
  4. Prefer higher-level primitives: Use Mutex, RwLock, etc. instead of raw Futex operations.
  5. Avoid busy-waiting: Use yield() sparingly; prefer proper synchronization primitives.
  6. Thread naming: Use setName() for easier debugging in profilers and debuggers.
  7. Error handling: Always handle SpawnError - thread creation can fail under resource pressure.

Platform Notes

Linux

  • Uses clone() syscall for thread creation without libc
  • Supports thread names up to 15 characters
  • Thread IDs are 32-bit PIDs

Windows

  • Uses CreateThread() Win32 API
  • Thread names up to 31 characters (via NtSetInformationThread)
  • Handles are HANDLE type

WASI

  • Requires allocator for thread spawning
  • Uses WebAssembly thread spawn interface
  • Limited platform support

POSIX/pthreads

  • Uses pthread_create() from libc
  • Portable across Unix-like systems
  • Platform-specific thread ID formats

Build docs developers (and LLMs) love