Zig C Threads
C threads are separate paths of execution created through the C library. The main path waits for each thread to finish, then reads the results.
One thread, one result
A single thread writes a value into a shared variable. The main path joins the thread, then prints the value.
const std = @import("std");
const c = @cImport({
@cInclude("pthread.h");
});
var result: c_int = 0;
fn worker(arg: ?*anyopaque) callconv(.C) ?*anyopaque {
_ = arg;
result = 42;
return null;
}
pub fn main() !void {
var thread: c.pthread_t = 0;
const rc1 = c.pthread_create(&thread, null, worker, null);
const rc2 = c.pthread_join(thread, null);
const out = std.io.getStdOut().writer();
try out.print("create={d} join={d} result={d}\n", .{ rc1, rc2, result });
}
Two threads, two slots
Two threads run the same function with different result slots. The main path joins both threads, then prints the sum.
const std = @import("std");
const c = @cImport({
@cInclude("pthread.h");
});
var slot_a: c_int = 0;
var slot_b: c_int = 0;
fn set_fifteen(arg: ?*anyopaque) callconv(.C) ?*anyopaque {
const slot: *c_int = @ptrCast(@alignCast(arg.?));
slot.* = 15;
return null;
}
pub fn main() !void {
var t1: c.pthread_t = 0;
var t2: c.pthread_t = 0;
_ = c.pthread_create(&t1, null, set_fifteen, @ptrCast(&slot_a));
_ = c.pthread_create(&t2, null, set_fifteen, @ptrCast(&slot_b));
_ = c.pthread_join(t1, null);
_ = c.pthread_join(t2, null);
const out = std.io.getStdOut().writer();
try out.print("sum={d}\n", .{slot_a + slot_b});
}
A thread computes a square
A thread reads an input value and writes its square into a result variable. The main path joins the thread, then prints the square.
const std = @import("std");
const c = @cImport({
@cInclude("pthread.h");
});
var input: c_int = 9;
var output: c_int = 0;
fn square(arg: ?*anyopaque) callconv(.C) ?*anyopaque {
_ = arg;
output = input * input;
return null;
}
pub fn main() !void {
var thread: c.pthread_t = 0;
_ = c.pthread_create(&thread, null, square, null);
_ = c.pthread_join(thread, null);
const out = std.io.getStdOut().writer();
try out.print("square={d}\n", .{output});
}
Example
const std = @import("std");
const c = @cImport({
@cInclude("pthread.h");
});
var dbl_in: c_int = 21;
var dbl_out: c_int = 0;
fn double(arg: ?*anyopaque) callconv(.C) ?*anyopaque {
_ = arg;
dbl_out = dbl_in * 2;
return null;
}
pub fn main() !void {
var thread: c.pthread_t = 0;
_ = c.pthread_create(&thread, null, double, null);
_ = c.pthread_join(thread, null);
const out = std.io.getStdOut().writer();
try out.print("doubled={d}\n", .{dbl_out});
}
Key points
- C thread: a C thread is a separate path of execution started with a C library call.
- Join: a join is a wait for a thread to finish before the main path reads its result.
- A thread function takes one pointer argument and returns one pointer value.
- A thread writes its result into memory the main path can read after the join.
- Threads never print in this lesson, so the printed output comes only from the main path after all joins.
- Each thread gets its own result slot, so two threads never write the same memory.
The example defines a thread function that doubles its input value. It starts the thread, waits for it with a join, and only then reads the result. Printing happens in the main path, never inside the thread.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
When may the main path read a result written by a C thread?