# ForkUnion for C

> NUMA-aware fork-join parallelism, from C

## Quickstart

__`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](#numa).
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`.

ForkUnion provides a pure C99 API via `forkunion/forkunion.h`, wrapping the C++ implementation in pre-compiled libraries: `libforkunion_static.a` or `libforkunion_shared.so`.
Installing also drops a `forkunion.pc`, so `pkg-config --cflags --libs forkunion` works outside CMake.
The C API uses opaque `fu_pool_t` handles and function pointers for callbacks, making it compatible with any C99+ compiler.

To integrate using CMake:

```cmake
FetchContent_Declare(
    forkunion
    GIT_REPOSITORY https://github.com/ashvardanian/ForkUnion
    GIT_TAG v4.0.0-rc.1
)
FetchContent_MakeAvailable(forkunion)
target_link_libraries(your_target PRIVATE forkunion::static)
```

A minimal C example:

```c
#include <stdio.h>                // printf
#include <forkunion/forkunion.h>  // fu_pool_t, fu_pool_new, fu_pool_spawn

void hello_callback(void *context, size_t thread, size_t compute_domain) {
    (void)context;
    printf("Hello from thread %zu (compute_domain %zu)\n", thread, compute_domain);
}

int main(void) {
    fu_topology_t topology;
    fu_pool_t pool;
    size_t cores;
    if (fu_topology_new(&topology) != fu_success_k) return 1;
    if (fu_pool_new("my_pool", fu_capabilities_all_k, &pool) != fu_success_k) return 1;
    if (fu_logical_cores_count(topology, &cores) != fu_success_k) return 1;
    if (fu_pool_spawn(topology, pool, cores, fu_caller_inclusive_k) != fu_success_k) return 1;

    if (fu_pool_for_threads(pool, hello_callback, NULL) != fu_success_k) return 1;
    fu_pool_delete(pool);
    fu_topology_delete(topology);
    return 0;
}
```

The `fu_`-prefixed functions report the [hardware topology](#topology), to size and place work:

```c
fu_topology_t topology;
size_t domains, cores, level, memory;
if (fu_topology_new(&topology) != fu_success_k) return 1;
if (fu_compute_domains_count(topology, &domains) != fu_success_k) return 1;
for (size_t domain = 0; domain < domains; ++domain) {
    fu_logical_cores_count_in(topology, domain, &cores);
    fu_compute_level_in(topology, domain, &level);
    fu_local_memory_of(topology, domain, &memory);
    printf("domain %zu: %zu cores, level %zu, allocate on memory domain %zu\n", domain, cores, level, memory);
}
fu_topology_delete(topology);
```

For parallel tasks with context:

```c
struct task_context {
    int *data;
    size_t size;
};

void process_task(void *ctx, size_t task, size_t thread, size_t compute_domain) {
    (void)thread; (void)compute_domain;
    struct task_context *context = (struct task_context *)ctx;
    context->data[task] = task * 2;
}

int main(void) {
    fu_topology_t topology;
    fu_pool_t pool;
    if (fu_topology_new(&topology) != fu_success_k) return 1;
    if (fu_pool_new("tasks", fu_capabilities_all_k, &pool) != fu_success_k) return 1;
    if (fu_pool_spawn(topology, pool, 4, fu_caller_inclusive_k) != fu_success_k) return 1;

    int data[100] = {0};
    struct task_context ctx = { .data = data, .size = 100 };
    fu_pool_for_n(pool, 100, process_task, &ctx);        // static scheduling
    fu_pool_for_n_dynamic(pool, 100, process_task, &ctx); // dynamic scheduling

    fu_pool_delete(pool);
    fu_topology_delete(topology);
    return 0;
}
```

### GCC Nested Functions Extension

GCC supports [nested functions](https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html) that can capture variables from the enclosing scope:

```c
#include <stdio.h>
#include <stdatomic.h>
#include <forkunion/forkunion.h>

int main(void) {
    fu_topology_t topology;
    fu_pool_t pool;
    if (fu_topology_new(&topology) != fu_success_k) return 1;
    if (fu_pool_new("gcc_nested", fu_capabilities_all_k, &pool) != fu_success_k) return 1;
    if (fu_pool_spawn(topology, pool, 4, fu_caller_inclusive_k) != fu_success_k) return 1;

    atomic_size_t counter = 0;

    // GCC nested function - captures 'counter' from enclosing scope
    void nested_callback(void *ctx, size_t task, size_t thread, size_t compute_domain) {
        (void)ctx; (void)thread; (void)compute_domain;
        atomic_fetch_add(&counter, 1);
    }

    fu_pool_for_n(pool, 100, nested_callback, NULL);
    printf("Completed %zu tasks\n", (size_t)atomic_load(&counter));

    fu_pool_delete(pool);
    fu_topology_delete(topology);
    return 0;
}
```

Compile: `gcc -std=c11 test.c -lforkunion_static -lstdc++ -lpthread`
The archive is C++ behind a C facade, so a C consumer names the C++ runtime itself — `-lc++` under Apple's toolchain.

### Clang Blocks Extension

Clang provides [blocks](https://clang.llvm.org/docs/BlockLanguageSpec.html) with `^{}` syntax:

```c
#include <stdio.h>
#include <stdatomic.h>
#include <Block.h>
#include <forkunion/forkunion.h>

typedef void (^task_block_t)(void *, size_t, size_t, size_t);

struct block_wrapper { task_block_t block; };

void block_wrapper_fn(void *ctx, size_t task, size_t thread, size_t compute_domain) {
    ((struct block_wrapper *)ctx)->block(NULL, task, thread, compute_domain);
}

int main(void) {
    fu_topology_t topology;
    fu_pool_t pool;
    if (fu_topology_new(&topology) != fu_success_k) return 1;
    if (fu_pool_new("clang_blocks", fu_capabilities_all_k, &pool) != fu_success_k) return 1;
    if (fu_pool_spawn(topology, pool, 4, fu_caller_inclusive_k) != fu_success_k) return 1;

    __block atomic_size_t counter = 0;

    task_block_t my_block = ^(void *c, size_t task, size_t t, size_t col) {
        (void)c; (void)t; (void)col;
        atomic_fetch_add(&counter, 1);
    };

    task_block_t heap_block = Block_copy(my_block);
    struct block_wrapper wrapper = { .block = heap_block };

    fu_pool_for_n(pool, 100, block_wrapper_fn, &wrapper);

    Block_release(heap_block);
    printf("Completed %zu tasks\n", (size_t)atomic_load(&counter));

    fu_pool_delete(pool);
    fu_topology_delete(topology);
    return 0;
}
```

Compile: `clang -std=c11 -fblocks test.c -lforkunion_static -lstdc++ -lpthread -lBlocksRuntime`, where Apple's toolchain needs `-lc++` and no `-lBlocksRuntime`.

## Topology

A machine is described along two independent axes.
A __compute domain__ is a bindable group of cores sharing a Quality-of-Service class _and_ locality — it is what a pool spawns onto, and the index a worker callback receives.
A __memory domain__ is a bank of memory with its own capacity and access cost — it is what the allocator targets, may be _cpuless_ like a CXL expander, and may serve several compute domains at once.
The axes stay separate because they don't line up: performance and efficiency cores often share one memory controller, and equally-fast cores are often split across cache clusters or sockets.

Each axis carries a __level__, a dense ordinal grouping domains of like performance.
Levels can be fewer than domains, since several domains may share one, and the two axes count in opposite directions: compute levels grow with performance, memory levels grow with _distance_, placing HBM below DDR and CXL above.
Two objects split the answers by provenance, not by axis: a `Topology` holds what the platform __declares__ — domains, cores, QoS classes, volumes, on both axes — while a `Fabric` holds what ForkUnion __observes__: per-edge latencies, bandwidths, and distances, and the per-medium tiers derived from them.
The pipeline is `harvest` all the way down: a `Topology` harvests the declared structure from the OS and stays immutable, a pool spawns on it, and a `Fabric` then harvests the observed performance from the silicon through that pool's pinned workers, pointer-chasing and streaming every reachable edge.
A memory level is a property of the medium, independent of the querying core: it keys on the best bandwidth any initiator sustains to the pool, ties split by the best latency, so a 3 TB/s HBM pool outranks DDR even at equal latency.

|                   | Compute axis            | Memory axis            |
| :---------------- | :---------------------- | :--------------------- |
| Count domains     | `compute_domains_count` | `memory_domains_count` |
| Count levels      | `compute_levels_count`  | `memory_levels_count`  |
| Level of a domain | `compute_level_in`      | `memory_level_in`      |
| Faster means      | __higher__              | __lower__              |

Names are spelled here as in Rust, which Mojo matches; C prefixes them with `fu_`, and Zig spells them in camelCase.
Rust, Zig, and Mojo all carry the domain coordinates as distinct types — `ComputeDomain`, `MemoryDomain`, and `MemoryDomainId` — while C passes plain indices.
Domain counts and compute levels answer from the `Topology`; memory levels and the per-edge magnitudes answer from a harvested `Fabric`, which C prefixes with `fu_fabric_`.

Beyond the level ordinals, two magnitudes describe a compute domain, both best-effort.
`compute_capacity_in` forwards the kernel's own rating of a core, read from `/sys/devices/system/cpu/cpuN/cpu_capacity`, normalized so the fastest core on the machine reads 1024.
Linux publishes that file on `arm64` and on Intel hybrids, and nothing like it exists on Windows or Apple, which read 0.
A kernel that rates every core alike rates nothing, so a tie also reads 0; Linux then splits the classes by their core PMUs, `cpu_core` and `cpu_atom` on Intel hybrids.
`compute_cache_bytes_per_core_in` reports each logical core's share of the deepest cache confined to a domain: one instance's bytes over the cores sharing it.
It is enumerated exactly rather than measured — from the per-core cache hierarchy in `sysfs` on Linux, CPUID leaves elsewhere on x86, `sysctl` on Apple, and the cache relationships on Windows.
Domains of identical throughput can sit behind very differently sized caches, so neither value follows from the other.
Treat both as hints, present only where the hardware volunteers them, and never as a number a program requires.

The memory axis carries its own magnitudes, all answered by the harvested `Fabric`.
`memory_distance` gives the relative cost of reaching a memory domain from a compute domain — 10 for local, higher for remote, on ACPI SLIT's scale but from measured ratios — while `memory_bandwidth` and `memory_latency` give the observed saturated read bandwidth and dependent-load latency per edge.
Back on the `Topology`, `volume_ram` and `volume_ram_in` report installed RAM, total or per domain, while `huge_pages_count_in` and `volume_huge_pages_in` report the free huge pages a domain can still back an allocation with, as a count or in bytes, and `huge_pages_count` and `volume_huge_pages` total them.
Like the compute magnitudes, these are best-effort: fabric queries read 0 before a harvest or on edges no pinned worker could reach.

Finally, `local_memory_of` bridges the axes, naming the memory domain a given compute domain should allocate from.
Where no topology is harvested, every query degrades to a single compute domain and a single memory domain rather than failing, so these loops need no conditional compilation.

## NUMA

Placing memory on a chosen domain is implemented on Linux via the `mbind` syscall, on FreeBSD via `domainset` policies, and on Windows via `VirtualAllocExNuma`, and degrades to a single domain elsewhere.
The portable `domain_allocator_t` alias picks the right backend per platform - `linux_numa_allocator_t`, `freebsd_numa_allocator_t`, `windows_numa_allocator_t`, or a plain aligned fallback - each an STL-compatible allocator bound to one `memory_domain_id_t`.

Let's say you are building a Big Data application — brute-forcing Vector Search with the [NumKong](https://www.unum.cloud/docs/numkong.md) library — on a 2 dual-socket CPU system, the kind that powers production [USearch](https://github.com/unum-cloud/USearch/pulls) deployments.
The first part of that program may be responsible for sharding the incoming stream of data between distinct memory regions.
That part, in our simple example will be single-threaded:

```cpp
#include <vector> // `std::vector`
#include <span> // `std::span`
#include <forkunion.hpp> // `linux_numa_allocator`, `machine_topology_t`, `distributed_pool_t`
#include <numkong/spatial.h> // `nk_angular_f32`, `nk_f64_t`

namespace fu = ashvardanian::forkunion;
using floats_alloc_t = fu::linux_numa_allocator<float>;

constexpr std::size_t dimensions = 768; /// Matches most BERT-like models
static std::vector<float, floats_alloc_t> first_half(floats_alloc_t(fu::memory_domain_id_t {0}));
static std::vector<float, floats_alloc_t> second_half(floats_alloc_t(fu::memory_domain_id_t {1}));
static fu::machine_topology_t machine_topology;
static fu::distributed_pool_t distributed_pool;

/// Dynamically shards incoming vectors across 2 nodes in a round-robin fashion.
void append(std::span<float, dimensions> vector) {
    bool put_in_second = first_half.size() > second_half.size();
    if (put_in_second) second_half.insert(second_half.end(), vector.begin(), vector.end());
    else first_half.insert(first_half.end(), vector.begin(), vector.end());
}
```

The concurrent part would involve spawning threads adjacent to every memory pool to find the best `search_result_t`.
The primary `search` function, in ideal world would look like this:

1. Each thread finds the best match within its "slice" of a compute domain, tracking the best distance and index in a local CPU register.
2. All threads in each compute domain atomically synchronize using a domain-local instance of `search_result_t`.
3. The main thread collects aggregates of partial results from all compute domains.

That is, however, overly complicated to implement.
Such tree-like hierarchical reductions are optimal in a theoretical sense.
Still, assuming the relative cost of spin-locking once at the end of a thread scope and the complexity of organizing the code, the more straightforward path is better.
A minimal example would look like this:

```cpp
/// On each compute domain we'll synchronize the threads
struct search_result_t {
    nk_f64_t best_distance {std::numeric_limits<nk_f64_t>::max()};
    std::size_t best_index {0};
};

inline search_result_t pick_best(search_result_t const& a, search_result_t const& b) noexcept {
    return a.best_distance < b.best_distance ? a : b;
}

/// Uses all CPU threads to search for the closest vector to the @p query.
search_result_t search(std::span<float, dimensions> query) {

    bool const need_to_spawn_threads = distributed_pool.threads_count() == 0;
    if (need_to_spawn_threads) {
        fu::status_t const harvested = machine_topology.harvest();
        assert(fu::succeeded(harvested) && "Failed to harvest NUMA topology");
        assert(machine_topology.memory_domains_count() == 2 && "Expected exactly 2 NUMA nodes");
        fu::status_t const spawned = distributed_pool.spawn(machine_topology);
        assert(fu::succeeded(spawned) && "Failed to spawn NUMA pools");
    }

    search_result_t result;
    fu::spin_mutex_t result_update; // ? Lighter `std::mutex` alternative w/out futexes

    std::size_t const first_vectors = first_half.size() / dimensions;
    std::size_t const total_vectors = first_vectors + second_half.size() / dimensions;

    auto slices = distributed_pool.for_slices(total_vectors,
        [&](fu::tasks_range_t tasks, fu::thread_in_domain_t) noexcept {

        search_result_t thread_local_result;
        for (std::size_t const task : tasks) {
            bool const in_second = task >= first_vectors;
            auto const &shard = in_second ? second_half : first_half;
            std::size_t const local_index = in_second ? task - first_vectors : task;

            nk_f64_t distance;
            nk_angular_f32(query.data(), shard.data() + local_index * dimensions, dimensions, &distance);
            thread_local_result = pick_best(thread_local_result, {distance, task});
        }

        // ! Still synchronizing over a shared mutex for brevity.
        std::lock_guard<fu::spin_mutex_t> lock(result_update);
        result = pick_best(result, thread_local_result);
    });
    slices.join();
    return result;
}
```

In a dream world, we would call `distributed_pool.for_n`, but there is no clean way to make the scheduling processes aware of the data distribution in an arbitrary application, so that's left to the user.
The `for_slices` helper pairs each run with a `fu::thread_in_domain_t` naming its compute domain, but slices follow the thread count rather than the shard boundary, so the callback above picks the shard per task.
For more flexibility around building higher-level low-latency systems, there are unsafe APIs expecting you to manually "join" the broadcasted calls: `unsafe_for_threads` returns a generation token, `is_complete` polls it without blocking, and `unsafe_join` blocks until that generation completes.

The manual two-vector sharding above is what the __symmetric allocators__ automate: `symmetric_memory_allocator_t` - `fu_allocate_symmetric` in C - maps one virtual range striped a slice per memory domain, and Rust, Zig, and Mojo wrap it as `ShardedArray<T>`, one shard per domain, and `ReplicatedArray<T>`, a full copy per domain for read-mostly data.
To place and coordinate pools by hand, `spawn_on` - `fu_pool_spawn_on` in C - pins a pool to a single compute domain, while `locate_thread_in` and `threads_count_in` map a global thread index to its domain and count the workers living there.

## Busy Waiting

Here's what "busy waiting" looks like in C++:

```cpp
while (!has_work_to_do())
    std::this_thread::yield();
```

On Linux, the `std::this_thread::yield()` translates into a `sched_yield` system call, which means context switching to the kernel and back.
Instead, you can replace the `standard_yield_t` wrapper with a platform-specific micro-wait instruction, which is much cheaper.
Those instructions, like [`WFET` on Arm](https://developer.arm.com/documentation/ddi0602/2025-03/Base-Instructions/WFET--Wait-for-event-with-timeout-), generally hint the CPU to transition to a low-power state.

| Wrapper           | ISA          | Instruction         | Privileges |
| :---------------- | :----------- | :------------------ | :--------- |
| `x86_pause_t`     | x86          | `PAUSE`             | R3         |
| `x86_tpause_t`    | x86+WAITPKG  | `UMONITOR`+`UMWAIT` | R3         |
| `arm64_yield_t`   | AArch64      | `YIELD`             | EL0        |
| `arm64_wfe_t`     | AArch64      | `LDAXR`+`WFE`       | EL0        |
| `arm64_wfet_t`    | AArch64+WFXT | `WFET`              | EL0        |
| `riscv64_pause_t` | RISC-V       | `PAUSE`             | U          |
| `riscv64_wrs_t`   | RISC-V+Zawrs | `WRS.STO`/`WRS.NTO` | U          |

No kernel calls.
No futexes.
Works in tight loops.

The `UMWAIT`, `WFE`, `WFET`, and `WRS` wrappers go a step further than a spin hint: they sleep the core until a store to the watched line, capped by a timeout only where the loop guards more than one word, so the pool parks a worker in a low-power state rather than burning the core.
`spin_mutex<fu::preferred_yield_t>` takes the same wrappers, while the default `spin_mutex_t` yields to the scheduler and Rust's `SpinMutex` spins on `core::hint::spin_loop` - a futex-free `std::mutex` alternative, and the lock used in the NUMA example above.
On WebAssembly threads the waiter is `wasm_wait_t`: a 32- or 64-bit watch sleeps in `memory.atomic.wait` until a `wasm_atomic_ref` releasing write notifies or 1 µs passes, so pair the two; uncapped waits and 1- or 2-byte watches spin.

## Parallel Iterators

For Rayon-style ergonomics, use the parallel iterator API with the `prelude`.
Unlike Rayon, ForkUnion's parallel iterators don't depend on the global state and allow explicit control over the thread pool and scheduling strategy.
For statically shaped workloads, the default static scheduling is more efficient:

```rust
use forkunion as fu;
use forkunion::prelude::*;

let topology = fu::Topology::new().expect("Failed to detect hardware topology");
let mut pool = fu::spawn(&topology, 4);
let mut data: Vec<usize> = (0..1000).collect();

(&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Value: {}", value);
    })?;
```

For dynamic work-stealing, use `with_schedule` with `DynamicScheduler`:

```rust
(&mut data[..])
    .into_par_iter()
    .with_schedule(&mut pool, DynamicScheduler)
    .for_each(|value, _task, _at| {
        *value *= 2;
    })?;
```

This easily composes with other iterator adaptors, like `map`, `filter`, and `zip`:

```rust
(&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .map(|x| x * x)
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Squared even: {}", value);
    })?;
```

For parallel reductions, ForkUnion provides Rayon-like convenience methods with automatic cache-aligned scratch allocation:

```rust
let data: Vec<u64> = (0..1_000_000).map(|i| i as u64).collect();

// Sum all elements with automatic scratch allocation
let total: u64 = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .sum()?;

// Count elements matching a predicate
let evens = (&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .with_pool(&mut pool)
    .count()?;

// Custom reduction (maximum)
let maximum = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce(
        || 0u64,                                     // initial value
        |acc, value, _, _| *acc = (*acc).max(*value), // fold function
        |a, b| a.max(b)                              // combine function
    )?;
```

For manual control over scratch allocation, use `reduce_with_scratch`:

```rust
// Cache-line aligned wrapper to prevent false sharing
let mut scratch: Vec<CacheAligned<u64>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

let total = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce_with_scratch(
        scratch.as_mut_slice(),
        |acc, value, _, _| acc.0 += *value,  // fold
        |a, b| a.0 += b.0                 // combine in-place
    )?;
```

Beyond reductions, the iterators offer short-circuiting searches - `find_first` and `find_last` for the deterministic lowest- or highest-index match, `find_any` for whichever match is found first with cooperative cancellation, and `any` / `all` for boolean predicates that stop the moment the answer is known.
Fallible bodies get `for_each_fallible` and `fold_with_scratch_fallible`, which propagate the first error and signal the other workers to stop.
Their two answers stay apart: the outer `Result` reports whether the dispatch ran, the inner one whether the caller's own work succeeded, so the caller's error type carries no constraint.
This Rayon-style layer is __Rust-only__; C, C++, Zig, and Mojo expose the pool primitives `for_threads`, `for_n`, `for_n_dynamic`, and `for_slices` directly.
Zig additionally mirrors Rust's `for_slices_mut` as `forSlicesMut`, which hands each thread a disjoint sub-slice rather than the `TasksRange` its indices would form.
