Zig bounded worker pool with atomic index

Zig
const std = @import("std");

fn square(x: u32) !u32 { return if (x < 1_000_000) x * x else error.TooLarge; }

fn runPool(alloc: std.mem.Allocator, input: []const u32, results: []?u32, errors: []?anyerror, workers: usize) !void {
    const Ctx = struct {
        input: []const u32, results: []?u32, errors: []?anyerror,
        next: std.atomic.Value(usize),
        fn work(self: *@This()) void {
            while (true) {
                const i = self.next.fetchAdd(1, .monotonic);
                if (i >= self.input.len) return;
                if (square(self.input[i])) |v| self.results[i] = v else |e| self.errors[i] = e;
            }
        }
    };
    var ctx = Ctx{ .input = input, .results = results, .errors = errors, .next = std.atomic.Value(usize).init(0) };
    const threads = try alloc.alloc(std.Thread, workers);
    defer alloc.free(threads);
    for (threads) |*t| t.* = try std.Thread.spawn(.{}, Ctx.work, .{&ctx});
    for (threads) |t| t.join();
}