A thread is an independent flow of execution.
A Zig program begins with one thread: the main thread. Additional threads may be created to perform work in parallel.
A thread runs a function.
Here is the smallest useful example:
const std = @import("std");
fn worker() void {
std.debug.print("worker thread\n", .{});
}
pub fn main() !void {
const thread = try std.Thread.spawn(.{}, worker, .{});
thread.join();
}Run it:
zig run main.zigThe output is:
worker threadstd.Thread.spawn creates a new operating system thread.
The first argument contains thread options:
.{}This example uses the default options.
The second argument is the function to run:
workerThe third argument is a tuple containing the function arguments:
.{}worker takes no parameters, so the tuple is empty.
The return value of spawn is a Thread object:
const thread = try std.Thread.spawn(...);The thread begins executing immediately.
The call:
thread.join();waits for the thread to finish.
If join is removed, the program may exit before the worker thread completes.
A thread function may take parameters:
const std = @import("std");
fn printNumber(n: u32) void {
std.debug.print("number = {d}\n", .{n});
}
pub fn main() !void {
const thread = try std.Thread.spawn(
.{},
printNumber,
.{42},
);
thread.join();
}The tuple:
.{42}provides the arguments passed to the thread function.
Multiple threads may run at the same time:
const std = @import("std");
fn worker(id: u32) void {
std.debug.print("worker {d} starting\n", .{id});
}
pub fn main() !void {
const t1 = try std.Thread.spawn(.{}, worker, .{1});
const t2 = try std.Thread.spawn(.{}, worker, .{2});
const t3 = try std.Thread.spawn(.{}, worker, .{3});
t1.join();
t2.join();
t3.join();
}The output order is not guaranteed.
One run may produce:
worker 1 starting
worker 2 starting
worker 3 startingAnother may produce:
worker 3 starting
worker 1 starting
worker 2 startingThreads execute concurrently. The operating system scheduler decides when each thread runs.
A thread shares memory with other threads in the same process.
This makes communication easy, but it also introduces problems.
Consider this program:
const std = @import("std");
var counter: u32 = 0;
fn increment() void {
var i: u32 = 0;
while (i < 100000) : (i += 1) {
counter += 1;
}
}
pub fn main() !void {
const t1 = try std.Thread.spawn(.{}, increment, .{});
const t2 = try std.Thread.spawn(.{}, increment, .{});
t1.join();
t2.join();
std.debug.print("{d}\n", .{counter});
}You might expect:
200000But the result is undefined.
Two threads modify counter at the same time. This is a data race.
A data race occurs when:
- Multiple threads access the same memory.
- At least one access writes.
- The accesses are not synchronized.
Correct threaded programs require synchronization.
Zig provides synchronization primitives in the standard library:
| Primitive | Purpose |
|---|---|
std.Thread.Mutex | Protect shared data |
std.Thread.RwLock | Shared and exclusive locking |
std.Thread.Condition | Wait for events |
std.atomic | Atomic memory operations |
std.Thread.Semaphore | Control resource access |
A mutex protects shared state:
const std = @import("std");
var counter: u32 = 0;
var mutex = std.Thread.Mutex{};
fn increment() void {
var i: u32 = 0;
while (i < 100000) : (i += 1) {
mutex.lock();
counter += 1;
mutex.unlock();
}
}
pub fn main() !void {
const t1 = try std.Thread.spawn(.{}, increment, .{});
const t2 = try std.Thread.spawn(.{}, increment, .{});
t1.join();
t2.join();
std.debug.print("{d}\n", .{counter});
}Now the result is predictable.
Only one thread may hold the mutex at a time.
The region between lock and unlock is called a critical section.
Thread creation is not free.
Creating too many threads can reduce performance because:
- threads consume memory
- context switching costs time
- synchronization adds overhead
Many programs use a fixed number of worker threads instead of creating threads continuously.
Zig does not hide the operating system threading model. A Zig thread maps closely to a native system thread.
This keeps the behavior understandable and predictable.
Exercise 18-1. Write a program that creates four threads. Each thread should print its identifier five times.
Exercise 18-2. Modify the counter example so each thread increments the counter one million times.
Exercise 18-3. Remove the mutex from the synchronized counter program. Run the program several times and compare the results.
Exercise 18-4. Write a function that computes the sum of part of an array. Use two threads to compute the total sum of a larger array.