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.
Configuration for thread spawning
Function to execute (must return void, !void, u8, !u8, or noreturn)
Arguments tuple to pass to the function
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
};
Size in bytes of the thread’s stack
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.
- Windows:
windows.HANDLE
- POSIX:
pthread_t
- Linux:
i32 (TID)
- WASI:
i32
Thread Identity
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
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.
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
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
getCpuCount
pub fn getCpuCount() CpuCountError!usize
Returns the platform’s view on the number of logical CPU cores available.
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.
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,
};
System-imposed thread limit reached (RLIMIT_NPROC, /proc/sys/kernel/threads-max, etc.)
Kernel cannot allocate sufficient memory
LockedMemoryLimitExceeded
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
-
Always join or detach: Failing to call
join() or detach() will leak resources.
-
Use appropriate stack sizes: Default 16MB may be too large for many threads. Adjust based on actual needs.
-
Match thread count to workload: Use
getCpuCount() for CPU-bound tasks, but consider more threads for I/O-bound work.
-
Prefer higher-level primitives: Use
Mutex, RwLock, etc. instead of raw Futex operations.
-
Avoid busy-waiting: Use
yield() sparingly; prefer proper synchronization primitives.
-
Thread naming: Use
setName() for easier debugging in profilers and debuggers.
-
Error handling: Always handle
SpawnError - thread creation can fail under resource pressure.
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