# Quickstart

> ForkUnion for Zig

__`ForkUnion`__ is dead-simple to use!
There is no nested parallelism, exception handling, or "future promises"; they are banned.
A pool runs one dispatch at a time and refuses any other with the `contended` status, including one from inside its own callbacks.
The thread pool itself has a few core operations:

- `spawn` to initialize worker threads, and
- `for_threads` to launch a blocking callback on all threads.

Higher-level APIs for index-addressable tasks are also available:

- `for_n` - for individual evenly-sized tasks,
- `for_n_dynamic` - for individual unevenly-sized tasks,
- `for_slices` - for contiguous runs of evenly-sized tasks, one per worker.

For additional flow control and tuning, following helpers are available:

- `nap` - to park workers on the CPU's address monitor, waking with no syscall,
- `sleep(microseconds)` - to block workers in the kernel for long idle periods, with 0 waiting for the next dispatch,
- `terminate` - to kill the threads before the destructor is called,
- `unsafe_for_threads` - to broadcast a callback without blocking, returning a generation token,
- `unsafe_join` - to block until the completion of a broadcasted generation,
- `is_complete` - to poll a generation token for completion without blocking.

Every dispatch is identified by a `generation` token.
On `caller_exclusive_k` pools you can dispatch, overlap your own work, poll `is_complete`, and join - the classic poll-then-join pattern.
On `caller_inclusive_k` pools the calling thread owes one slice of the work, which only runs inside `unsafe_join`, so completion can't be reached by polling alone.
The same rule shapes the RAII guard returned by `for_threads` and, in C++, the `for_n` family: the workers start at the guard's construction on every pool, and on inclusive pools the caller's own slice runs at `join` or destruction.

On `caller_exclusive_k` pools thread 0 may also fork from inside its own slice, at any depth: such a fork waits for every other slice of the one enclosing it, runs on all threads, and returns complete.
So `unsafe_for_threads` can hand a whole multi-step call to thread 0, which forks the rest of the pool as it goes, while the caller only polls a status word the call stores at its end.
That is how one manager thread keeps several exclusive pools busy with no queue and no extra thread.
A join on such a pool waits for whatever dispatch runs, since the call may end before its submitter stores the token.

In C++ it provides [NUMA extensions](https://www.unum.cloud/docs/forkunion/zig/numa.md).
That includes the `colocated_pool` analog of the `flat_pool` and the `domain_allocator_t` for allocating memory in a specific memory domain.
Those are out-of-the-box compatible with the higher-level APIs.
Most interestingly, for Big Data applications, a higher-level `distributed_pool` class addresses and balances the work across all compute domains.

> `domain_allocator_t` is backed by `linux_numa_allocator_t`, `freebsd_numa_allocator_t`, or `windows_numa_allocator_t`.

To integrate into your Zig project, let `zig fetch` pin the dependency and its content hash into `build.zig.zon`:

```bash
zig fetch --save=forkunion https://github.com/ashvardanian/ForkUnion/archive/refs/tags/v4.0.0-rc.1.tar.gz
```

That records the dependency; `build.zig` still has to wire it, and the module carries the compiled artifact so a consumer does not relink it by hand:

```zig
const forkunion = b.dependency("forkunion", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("forkunion", forkunion.module("forkunion"));
```

Then import and use in your code:

```zig
const std = @import("std");
const fu = @import("forkunion");

pub fn main() !void {
    const topology = try fu.Topology.init();
    defer topology.deinit();
    const pool = try fu.Pool.init(topology, .{ .threads = 4 });
    defer pool.deinit();

    // Execute work on each thread (OpenMP-style parallel)
    try pool.forThreads({}, struct {
        fn work(thread_index: usize, compute_domain: fu.ComputeDomain) void {
            _ = compute_domain;
            std.debug.print("Thread {}\n", .{thread_index});
        }
    }.work);

    // Distribute 1000 tasks across threads (OpenMP-style parallel for)
    var results = [_]i32{0} ** 1000;
    try pool.forN(1000, &results, struct {
        fn process(slots: *[1000]i32, task: usize, at: fu.ThreadInDomain) void {
            _ = at;
            slots[task] = @intCast(task * 2);
        }
    }.process);

    // Or let the pool hand each thread a disjoint sub-slice, with no index arithmetic
    try pool.forSlicesMut(i32, &results, {}, struct {
        fn scale(chunk: []i32, at: fu.ThreadInDomain) void {
            _ = at;
            for (chunk) |*slot| slot.* *= 2;
        }
    }.scale);
}
```

The context comes before the callback and travels as a caller-owned pointer, so the callback receives exactly the qualifiers the caller chose — the same shape `std.sort` uses, and the reason a `std.atomic.Value` in a context can never be silently copied per task.
Pass `{}` when a kernel needs no state at all.

The `Topology` handle reports the [hardware topology](https://www.unum.cloud/docs/forkunion/zig/topology.md), to size and place work:

```zig
const topology = try fu.Topology.init();
defer topology.deinit();
for (0..try topology.computeDomainsCount()) |index| {
    const domain = fu.ComputeDomain.at(index);
    std.debug.print("domain {d}: {d} cores, level {d}, allocate on memory domain {d}\n", .{
        index, try topology.logicalCoresCountIn(domain), try topology.computeLevelIn(domain), (try topology.localMemoryOf(domain)).index(),
    });
}
```

A compute domain, a memory domain, and a memory domain's OS id are three distinct types — `ComputeDomain`, `MemoryDomain`, and `MemoryDomainId` — exactly as in Rust and Mojo, so passing one where another belongs is a compile error rather than a silent read of the wrong node.

The Zig standard library no longer ships a fork-join pool at all; its replacement, `std.Io.Group`, allocates per task, grows its thread set lazily, runs work inline on the caller once saturated, and knows nothing about NUMA.

`const forkunion = @import("forkunion");`

`const Capabilities = @import("forkunion").Capabilities;`

`const Status = @import("forkunion").Status;`

`const IndexedSplit = @import("forkunion").IndexedSplit;`

`const TasksRange = @import("forkunion").TasksRange;`

`const SyncConstPtr = @import("forkunion").SyncConstPtr;`

`const SyncMutPtr = @import("forkunion").SyncMutPtr;`

`const Pool = @import("forkunion").Pool;`

`const PoolOptions = @import("forkunion").PoolOptions;`

## CallerExclusivity

Defines whether the calling thread participates in task execution

```zig
pub const CallerExclusivity = enum(c_int)
```

- `inclusive` = `0`: Calling thread participates in workload, spawning N-1 workers.
- `exclusive` = `1`: Calling thread only coordinates, spawning N workers.

## Error

One error per status, so `@errorName` carries as much as the status itself does.

```zig
pub const Error = error
```

Zig error values hold no payload, so the symbol name that named which call failed cannot travel
with the error. Reach for [`Status`](#Status) where that detail matters.

- `Unknown`
- `BadAlloc`
- `CapacityExhausted`
- `InvalidArgument`
- `ConfigMismatch`
- `AlreadySpawned`
- `NotSpawned`
- `ThreadRefused`
- `TopologyUnavailable`
- `PermissionDenied`
- `Unsupported`
- `Contended`

## Capabilities

Everything the library can do, whether decided when it was compiled or found on this machine.

```zig
pub const Capabilities = packed struct(u32)
```

One bit per facility, and two accessors ask two questions of the same bit.
[`comptimeCapabilities`](#comptimeCapabilities) reports whether the code was _built_: a set `place_huge_pages_on_domain`
means we compiled the path that asks for them. [`runtimeCapabilities`](#runtimeCapabilities) reports whether the machine
_offers_ it now.

Neither implies the other. A binary that built `place_memory_on_domain` runs perfectly well on a
single-node box, where the runtime accessor never sets that bit; and a machine with four NUMA
nodes reports none of them to a build that left the topology out.

- `x86_pause` `bool`: x86 `pause` instruction
- `x86_tpause` `bool`: x86-64 `tpause` instruction, with `WAITPKG` support
- `arm64_yield` `bool`: Arm `yield` instruction
- `arm64_wfet` `bool`: AArch64 `wfet` instruction, with `FEAT_WFxT` support
- `riscv64_pause` `bool`: RISC-V `pause` instruction
- `riscv64_wrs` `bool`: RISC-V `WRS.STO` monitored wait, from the `Zawrs` extension
- `os_threads` `bool`: Own the raw OS thread handle instead of a `std::thread`
- `topology` `bool`: Enumerate this machine's cores, compute domains, and memory domains
- `place_threads_by_affinity` `bool`: Bind a thread to a set of cores, choosing where it runs
- `place_threads_by_core_class` `bool`: Steer a thread onto a class of core at creation, choosing where it runs
- `reschedule_threads_by_class` `bool`: Reclass a thread's scheduler to sleep or wake it, choosing when it runs
- `place_memory_on_domain` `bool`: Place a buffer's pages on a chosen memory domain
- `place_huge_pages_on_domain` `bool`: Place larger-than-base pages on a chosen memory domain
- `huge_transparent_pages` `bool`: The kernel promotes base pages to huge pages on its own
- `colocate_pools_on_domain` `bool`: The domain-aware `colocated_pool` and `distributed_pool` are compiled in
- `x86_cldemote` `bool`: `CLDEMOTE` moves a just-written line toward the shared LLC and retains it. Reporting-only: the emitter is chosen at compile time by `FORKUNION_WITH_DEMOTE_CACHE_LINES`, never dispatched.
- `arm64_dc_cvac` `bool`: `DC CVAC` cleans a dirty line to the coherency point - AArch64's nearest demote. Set where EL0 execution is known-legal, i.e. Linux, which sets `SCTLR_EL1.UCI`.
- `riscv64_zicbom` `bool`: The kernel enabled user-mode Zicbom cache-block management, attested through `hwprobe` - the bit the C ABI's cascade selects `cbo.clean` on.
- `x86_cmpccxadd` `bool`: `CMPCCXADD`, the conditional atomic add, admitting `x86_cmpccxadd_atomic_ref`.
- `x86_raoint` `bool`: RAO-INT `aadd` and its family, executed at the shared cache.
- `arm64_lse` `bool`: Armv8.1 `FEAT_LSE`: one instruction per read-modify-write.
- `arm64_rcpc` `bool`: Armv8.3 `FEAT_LRCPC`: `LDAPR` acquires, cheaper than `LDAR`'s.
- `riscv64_zacas` `bool`: RISC-V Zacas, the `amocas` compare-and-swap.
- `riscv64_atomic` `bool`: The RISC-V A extension's `lr`, `sc` and `amo` words.
- `arm64_prfm` `bool`: `PRFM`, AArch64's prefetch hint.
- `riscv64_zicbop` `bool`: RISC-V Zicbop, the `prefetch.w` hint.
- `riscv64_zabha` `bool`: RISC-V Zabha, byte and halfword atomics.
- `_unused` `u5`

## all

All-ones allow-mask: pass to a pool constructor to disable capability filtering.

```zig
pub fn all() Capabilities
```

## Status

Why a call into the C core failed, mirroring `fu_status_t` value-for-value.

```zig
pub const Status = enum(c_int)
```

Non-exhaustive, so a status a newer core reports arrives intact rather than coerced.

- `success` = `0`
- `unknown` = `-1`
- `bad_alloc` = `-2`
- `capacity_exhausted` = `-3`
- `invalid_argument` = `-4`
- `config_mismatch` = `-5`
- `already_spawned` = `-6`
- `not_spawned` = `-7`
- `thread_refused` = `-8`
- `topology_unavailable` = `-9`
- `permission_denied` = `-10`
- `unsupported` = `-11`
- `contended` = `-14`

## describe

Static, English description; mirrors `fu_status_to_string`.

```zig
pub fn describe(self: Status) [:0]const u8
```

## IndexedSplit

Splits a range of tasks into fair-sized runs for parallel distribution.

```zig
pub const IndexedSplit = struct
```

The first `tasks % threads` runs get `ceil(tasks / threads)` tasks; the rest get `floor(tasks / threads)`. This minimizes size variance across threads. Mirrors the C++ `indexed_split` and
Lemire's fair-run scheme.
See: https://lemire.me/blog/2025/05/22/dividing-an-array-into-fair-sized-chunks/

- `quotient` `usize`
- `remainder` `usize`

## init

Builds a split of `tasks_count` tasks across `threads_count` nonzero threads.

```zig
pub fn init(tasks_count: usize, threads_count: usize) IndexedSplit
```

## get

Returns the `{ first, count }` run owned by thread `thread_index`.

```zig
pub fn get(self: IndexedSplit, thread_index: usize) TasksRange
```

## TasksRange

A half-open `[first, first + count)` run of task indices - the "what work" of a slice dispatch.

```zig
pub const TasksRange = struct
```

Zig has no first-class range value, so `first` and `count` feed the language's own `for` syntax:
`for (range.first..range.end()) |task|`. An idle thread receives `count == 0`, and every
dispatch still calls it exactly once.

- `first` `usize`: The first task index in the run.
- `count` `usize`: How many tasks the run covers; zero means an idle thread.

## end

One past the last task index, so `first..end()` is the half-open span.

```zig
pub fn end(self: TasksRange) usize
```

## isEmpty

Whether the run covers no tasks at all.

```zig
pub fn isEmpty(self: TasksRange) bool
```

## of

The run's slice of `data`, which must be at least `end()` long.

```zig
pub fn of(self: TasksRange, comptime T: type, data: []T) []T
```

## SyncConstPtr

A `Send + Sync`-style read-only raw-pointer view, letting one immutable buffer be read by every worker across the FFI callback boundary.

```zig
pub fn SyncConstPtr(comptime T: type) type
```

The caller guarantees the pointee outlives the parallel region and is not mutated while shared.

- `ptr` `[*]const T`

## init

Wraps a raw pointer to the start of the shared buffer.

```zig
pub fn init(ptr: [*]const T) Self
```

## get

A reference to the element at [`index`](https://www.unum.cloud/docs/forkunion/zig/topology.md#index); the caller owns bounds checking.

```zig
pub fn get(self: Self, index: usize) *const T
```

## as*

The underlying raw pointer.

```zig
pub fn asPtr(self: Self) [*]const T
```

## SyncMutPtr

A `Send + Sync`-style mutable raw-pointer view, letting workers write disjoint slots of one buffer across the FFI callback boundary.

```zig
pub fn SyncMutPtr(comptime T: type) type
```

The caller guarantees each worker touches disjoint indices and the pointee outlives the region.

- `ptr` `[*]T`

## init

Wraps a raw pointer to the start of the shared buffer.

```zig
pub fn init(ptr: [*]T) Self
```

## get

A mutable pointer to the element at [`index`](https://www.unum.cloud/docs/forkunion/zig/topology.md#index); the caller owns disjointness and bounds.

```zig
pub fn get(self: Self, index: usize) *T
```

## Pool

Thread pool for fork-join parallelism

```zig
pub const Pool = struct
```

- `handle` `*anyopaque`

## init

Spawns a pool onto `topo`, per `options`.

```zig
pub fn init(topo: Topology, options: PoolOptions) Error!Pool
```

Spawn one pool per compute domain with `.placement = .{ .on_compute_domain = … }` and
coordinate them from a single thread with the generation-token API.

## deinit

Destroys the thread pool

```zig
pub fn deinit(self: Pool) void
```

## threadsCount

Returns the number of threads in the pool

```zig
pub fn threadsCount(self: Pool) Error!usize
```

## callerExclusivity

Returns whether the calling thread participates in the workload.

```zig
pub fn callerExclusivity(self: Pool) Error!CallerExclusivity
```

Queries the pool directly rather than caching, so it stays correct across [`terminate`](#terminate) and
re-spawning with a different exclusivity.

## capabilities

Returns the capabilities this pool actually spawned with - the allow-mask intersected with what the build compiled and the machine offers, so a NUMA-less box reports the flat pool.

```zig
pub fn capabilities(self: Pool) Error!Capabilities
```

## computeDomainsCount

Returns the number of compute domains the pool spans

```zig
pub fn computeDomainsCount(self: Pool) Error!usize
```

## threadsCountIn

Returns the number of threads in a specific compute domain

```zig
pub fn threadsCountIn(
    self: Pool,
    compute_domain: ComputeDomain,
) Error!usize
```

## locateThreadIn

Converts a global thread index to its local index within a compute domain

```zig
pub fn locateThreadIn(
    self: Pool,
    global_thread_index: usize,
    compute_domain: ComputeDomain,
) Error!usize
```

## terminate

Terminates all worker threads; the pool can be respawned.

```zig
pub fn terminate(self: Pool) void
```

Not thread-safe; call only when no dispatch is in flight, never as a sync point.

## nap

Parks worker threads on the CPU's address monitor, staying on their cores.

```zig
pub fn nap(self: Pool) void
```

Not thread-safe; call between task batches, and the next dispatch wakes the workers.

## sleep

Blocks worker threads in the kernel, freeing their cores, for at most `microseconds` between re-checks, or until the next dispatch when 0.

```zig
pub fn sleep(self: Pool, microseconds: usize) void
```

Not thread-safe; call between task batches, and the next dispatch wakes the workers.

## forThreads

Executes a callback on all threads, blocking.

```zig
pub fn forThreads(
    self: Pool,
    context: anytype,
    comptime func: anytype,
) Error!void
```

The context is a single-item pointer the caller owns, forwarded to the callback verbatim, so
its `const`-ness carries through:

- If context is `void`: `fn (usize, ComputeDomain) void`

- Otherwise: `fn (@TypeOf(context), usize, ComputeDomain) void`

## forN

Distributes N tasks across threads with static scheduling, blocking.

```zig
pub fn forN(
    self: Pool,
    n: usize,
    context: anytype,
    comptime func: anytype,
) Error!void
```

- If context is `void`: `fn (usize, ThreadInDomain) void`

- Otherwise: `fn (@TypeOf(context), usize, ThreadInDomain) void`

## forNDynamic

Distributes N tasks with dynamic work-stealing, blocking.

```zig
pub fn forNDynamic(
    self: Pool,
    n: usize,
    context: anytype,
    comptime func: anytype,
) Error!void
```

- If context is `void`: `fn (usize, ThreadInDomain) void`

- Otherwise: `fn (@TypeOf(context), usize, ThreadInDomain) void`

## forSlices

Distributes N tasks as slices, blocking.

```zig
pub fn forSlices(
    self: Pool,
    n: usize,
    context: anytype,
    comptime func: anytype,
) Error!void
```

The [`TasksRange`](#TasksRange) names the half-open run of task indices this worker drew.

- If context is `void`: `fn (TasksRange, ThreadInDomain) void`

- Otherwise: `fn (@TypeOf(context), TasksRange, ThreadInDomain) void`

## forSlicesMut

Splits `data` into one contiguous chunk per thread and runs `func` on each, blocking.

```zig
pub fn forSlicesMut(
    self: Pool,
    comptime T: type,
    data: []T,
    context: anytype,
    comptime func: anytype,
) Error!void
```

The chunks partition `data`, so no two threads observe overlapping elements and the callback
can write its own slice with no atomics and no index arithmetic.

- If context is `void`: `fn ([]T, ThreadInDomain) void`

- Otherwise: `fn (@TypeOf(context), []T, ThreadInDomain) void`

## unsafeForThreads

Executes callback on all threads without blocking; unsafe.

```zig
pub fn unsafeForThreads(
    self: Pool,
    context: anytype,
    comptime func: anytype,
) Error!usize
```

Returns a generation token to pass to [`isComplete`](#isComplete) or [`unsafeJoin`](#unsafeJoin).

- If context is `void`: `fn (usize, ComputeDomain) void`

- Otherwise: `fn (@TypeOf(context), usize, ComputeDomain) void`

Unlike the blocking dispatches, this one returns while worker threads may still be running,
so the pointee must outlive [`unsafeJoin`](#unsafeJoin) rather than merely the call.

## isComplete

Returns true if the given generation has completed.

```zig
pub fn isComplete(self: Pool, generation: usize) Error!bool
```

A `true` result also guarantees visibility of every contributor's writes. On
caller-inclusive pools this can only turn `true` once [`unsafeJoin`](#unsafeJoin) contributes the calling
thread's slice, so poll-then-join is reserved for exclusive pools.

## unsafeJoin

Blocks until the given generation completes; unsafe.

```zig
pub fn unsafeJoin(self: Pool, generation: usize) void
```

On caller-inclusive pools this also executes the calling thread's slice.
Idempotent: joining an already-joined generation returns immediately.

## PoolOptions

How a pool is spawned: width, placement, and the facilities it may use.

```zig
pub const PoolOptions = struct
```

One options struct rather than a family of constructors, so a call site names only what it
changes and a new knob costs no new overload.

- `threads` `usize`: Threads to run, the calling thread included when `exclusivity` is `.inclusive`.
- `placement` `Placement`: Which cores the pool may use: every compute domain, or exactly one.
- `name` `?[]const u8`: Optional thread name, clipped by the core to what the platform's thread naming accepts.
- `exclusivity` `CallerExclusivity`: Whether the calling thread runs work or only coordinates.
- `allowed` `Capabilities`: Allow-mask: clear a waiter bit to force a lower-priority busy-wait, or clear `place_memory_on_domain` to force the flat, non-NUMA pool.

## Placement

Both placements are named, so "every domain" is a state rather than a missing index.

```zig
pub const Placement = union(enum)
```

- `everywhere` `void`: Spread across every compute domain the topology reports.
- `on_compute_domain` `ComputeDomain`: Pin threads and domain-local allocations to one compute domain. On builds without NUMA only compute domain 0 is valid.

## version

Returns the library version as a struct

```zig
pub fn version() struct { major: u32, minor: u32, patch: u32 }
```

## comptimeCapabilities

Which kernel facilities this build of ForkUnion was compiled to use.

```zig
pub fn comptimeCapabilities() Capabilities
```

## comptimeCapabilitiesString

The set [`comptimeCapabilities`](#comptimeCapabilities) bits, comma-separated, like `"threads,topology"`.

```zig
pub fn comptimeCapabilitiesString(buf: []u8) Error![]const u8
```

POLISH: writes into a caller-provided buffer so the returned slice borrows `buf`; the buffer
must outlive the slice. Callers pass their own stack buffer.

## runtimeCapabilities

Which features this machine turned out to offer, probing the CPU and the memory system.

```zig
pub fn runtimeCapabilities() Capabilities
```

## runtimeCapabilitiesString

The set [`runtimeCapabilities`](#runtimeCapabilities) bits, comma-separated: `"arm64_yield,place_memory_on_domain"`.

```zig
pub fn runtimeCapabilitiesString(buf: []u8) Error![]const u8
```

POLISH: writes into a caller-provided buffer so the returned slice borrows `buf`; the buffer
must outlive the slice. Callers pass their own stack buffer.

## default_alignment

The width the padding wrapper aligns to: two cache lines, because most x86 parts prefetch in pairs, so one line of separation still leaves two accumulators sharing a prefetch unit.

```zig
pub const default_alignment: usize
```

The core spells this `default_alignment_k` and deliberately does not use
`std::hardware_destructive_interference_size`, which trips GCC ABI warnings and picks worse.

Derived from the target the same way the header and `build.rs` derive it, so all three agree
without copying a number between them. Only padding rests on it - a pool strides its cells by
the width the machine reports at spawn.

