🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Thread Pools

A thread pool is a small set of worker threads that runs many jobs for you. You hand jobs to the pool instead of spawning one thread per job.

A pool runs three jobs

A pool with two workers runs three squaring jobs. Deinit waits for all three jobs, then the main thread prints the slots.

const std = @import("std");
fn square(slot: *i32, n: i32) void {
    slot.* = n * n;
}
pub fn main() !void {
    var out = std.io.getStdOut().writer();
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    var results: [3]i32 = .{ 0, 0, 0 };
    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = arena.allocator(), .n_jobs = 2 });
    try pool.spawn(square, .{ &results[0], 2 });
    try pool.spawn(square, .{ &results[1], 3 });
    try pool.spawn(square, .{ &results[2], 4 });
    pool.deinit();
    try out.print("squares={d} {d} {d}\n", .{ results[0], results[1], results[2] });
}

Jobs handed over in a loop

A loop hands four doubling jobs to the pool. Each job writes its own slot, and the printed order follows the slots.

const std = @import("std");
fn double(slot: *i32, n: i32) void {
    slot.* = n * 2;
}
pub fn main() !void {
    var out = std.io.getStdOut().writer();
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    var results: [4]i32 = .{ 0, 0, 0, 0 };
    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = arena.allocator(), .n_jobs = 2 });
    for (0..4) |i| {
        try pool.spawn(double, .{ &results[i], @as(i32, @intCast(i + 1)) });
    }
    pool.deinit();
    try out.print("doubled={d} {d} {d} {d}\n", .{ results[0], results[1], results[2], results[3] });
}

More jobs than workers

Six jobs run on a pool of two workers. The pool queues jobs the workers cannot run yet, and deinit waits for them all.

const std = @import("std");
fn timesTen(slot: *i32, n: i32) void {
    slot.* = n * 10;
}
pub fn main() !void {
    var out = std.io.getStdOut().writer();
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    var results: [6]i32 = .{ 0, 0, 0, 0, 0, 0 };
    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = arena.allocator(), .n_jobs = 2 });
    for (0..6) |i| {
        try pool.spawn(timesTen, .{ &results[i], @as(i32, @intCast(i)) });
    }
    pool.deinit();
    try out.print("slots={d} {d} {d} {d} {d} {d}\n", .{ results[0], results[1], results[2], results[3], results[4], results[5] });
}

Example

const std = @import("std");
fn square(slot: *i32, n: i32) void {
    slot.* = n * n;
}
pub fn main() !void {
    var out = std.io.getStdOut().writer();
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    var results: [5]i32 = .{ 0, 0, 0, 0, 0 };
    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = arena.allocator(), .n_jobs = 3 });
    for (0..5) |i| {
        try pool.spawn(square, .{ &results[i], @as(i32, @intCast(i + 1)) });
    }
    pool.deinit();
    var total: i32 = 0;
    for (results) |v| {
        total += v;
    }
    try out.print("squares={d} {d} {d} {d} {d} total={d}\n", .{ results[0], results[1], results[2], results[3], results[4], total });
}

Key points

  • Thread pool: a thread pool is a small set of worker threads that runs many jobs for you.
  • Job: a job is one function call the pool runs on a worker thread.
  • std.Thread.Pool is the pool type, and it needs an allocator to hold its workers and queued jobs.
  • pool.spawn hands one job to the pool, and the pool runs it on a worker thread.
  • pool.deinit stops the pool and waits until every queued job has finished.
  • Each job writes to its own slot, so the results stay in order no matter which worker ran which job.
  • The main thread prints results only after deinit, so every job is certainly done.

Five squaring jobs run on a pool of three workers. Each job writes its own slot, so the printed order never depends on which worker ran which job. The main thread prints the slots and their total only after deinit has waited for every job.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What does pool.deinit do in these lessons?