C · Quickstart

Quickstart

#include <forkunion/forkunion.h> 4.0.0-rc.1 · d927445f

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. 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:

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:

#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, to size and place work:

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:

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 that can capture variables from the enclosing scope:

#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 with ^{} syntax:

#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.

API

Types

fu_bool_tBoolean type: 0 for false, non-zero for true.
forkunion.h:115 ↗
typedef int fu_bool_t;
fu_status_tWhy a call failed, in a vocabulary the core and every binding share.
forkunion.h:134 ↗
enum fu_status_t

Success is 0; every failure is negative, so status < 0 and status != fu_success_k agree. Values are fixed and portable, so a binding may hard-code them.

Some enumerators name failures the core never returns itself: a binding that rejects a call before it crosses reports the same status the core would if it could see the same thing.

Statuses fall into four categories, with no interruptions to report. Resources: fu_bad_alloc_k, fu_thread_refused_k, fu_topology_unavailable_k, fu_permission_denied_k, fu_library_missing_k and fu_symbol_missing_k. Input: fu_invalid_argument_k. State: fu_capacity_exhausted_k, fu_config_mismatch_k, fu_already_spawned_k, fu_not_spawned_k, fu_unsupported_k and fu_contended_k. Defects: fu_unknown_k. Any verb that allocates, binds a device or touches a file may also return a Resources status. A verb's own block lists only what its contract adds.

Values

fu_success_k0The call completed and any output pointer holds a meaningful value.
fu_unknown_k-1No reason was reported, or one this build does not name.
fu_bad_alloc_k-2An allocation or mapping failed; a smaller request may succeed.
fu_capacity_exhausted_k-3A fixed ceiling was reached, so a smaller request will not help either.
fu_invalid_argument_k-4An argument was malformed, out of range, or would overflow a byte count.
fu_config_mismatch_k-5The handles or the pool kind cannot serve this call together.
fu_already_spawned_k-6The pool is already spawned; terminate it first.
fu_not_spawned_k-7The pool holds no workers: it was never spawned, or was terminated since.
fu_thread_refused_k-8The OS declined to create a thread - a resource limit, or permissions.
fu_topology_unavailable_k-9The machine could not be described; transient if its CPU set changed mid-probe.
fu_permission_denied_k-10A privileged operation was declined.
fu_unsupported_k-11This build or this machine has no such facility.
fu_library_missing_k-12Binding-only: nothing to load, or a different major version.
fu_symbol_missing_k-13Binding-only: the loaded core is missing a symbol.
fu_contended_k-14The pool is running another dispatch: another caller's, or the one this call runs inside.
fu_pool_tOpaque, cross-platform thread-pool handle, either flat, colocated, or distributed.
forkunion.h:198 ↗
typedef void * fu_pool_t;
fu_lambda_context_tType-punned pointer to a user-defined callback context.
forkunion.h:204 ↗
typedef void * fu_lambda_context_t;
fu_for_threads_tCallback type for thread-level operations.
forkunion.h:215 ↗
typedef void(*) fu_for_threads_t(fu_lambda_context_t context, size_t thread, size_t compute_domain);
fu_for_task_tCallback type for task-level operations receiving individual indices.
forkunion.h:224 ↗
typedef void(*) fu_for_task_t(fu_lambda_context_t context, size_t task, size_t thread, size_t compute_domain);
fu_for_range_tCallback type for slice-level operations receiving ranges of tasks.
forkunion.h:234 ↗
typedef void(*) fu_for_range_t(fu_lambda_context_t context, size_t first, size_t count, size_t thread, size_t compute_domain);
fu_caller_exclusivity_tDefines the in- and exclusivity of the calling thread for the executing task.
forkunion.h:244 ↗
enum fu_caller_exclusivity_t

This enum affects how the join is performed. If the caller is inclusive, 1/Nth of the work runs on the calling thread, and the join happens in its own scope.

Values

fu_caller_inclusive_kThe calling thread participates in the workload.
fu_caller_exclusive_kThe calling thread only coordinates, doesn't execute tasks.

See also

fu_capabilities_tDescribes all the special library features, both those compiled in and those found here.
forkunion.h:270 ↗
enum fu_capabilities_t

One bit per facility, and two accessors ask two questions of the same bit. fu_comptime_capabilities reports whether the code was built: a set fu_capability_place_huge_pages_on_domain_k means we compiled the path that asks for them. fu_runtime_capabilities reports whether the machine offers it now.

Neither implies the other. A binary that built fu_capability_place_memory_on_domain_k 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.

Values

fu_capabilities_unknown_k0No bit set: nothing requested, and nothing yet found.
fu_capability_x86_pause_k1 << 0The PAUSE spin hint, on every x86 since the Pentium 4: a short pipeline stall that keeps a busy-wait from flooding the load ports and eases the sibling hardware thread.
fu_capability_x86_tpause_k1 << 1TPAUSE sleeps the core until a deadline rather than spinning - the WAITPKG feature, the CPUID leaf 7, subleaf 0, ECX bit 5, on Sapphire Rapids, Alder Lake and their successors.
fu_capability_arm64_yield_k1 << 2The YIELD hint, on every AArch64: releases the pipeline to a sibling hardware thread and costs nothing where there is none.
fu_capability_arm64_wfet_k1 << 3WFET sleeps the core until a deadline or an event on the monitored line - FEAT_WFxT, Armv8.7, read from ID_AA64ISAR2_EL1 on Linux and hw.optional.arm.FEAT_WFxT on Apple.
fu_capability_riscv64_pause_k1 << 4The PAUSE spin hint of the Zihintpause extension - encoded as a FENCE every hart accepts, so it is reported on every RISC-V.
fu_capability_riscv64_wrs_k1 << 5WRS.STO sleeps the hart until a reservation breaks or a short timeout - the Zawrs extension, attested by the kernel's hwprobe.
fu_capability_os_threads_k1 << 6Own the raw OS thread handle instead of a std::thread - the substrate every thread lever below stands on.
fu_capability_topology_k1 << 7Enumerate this machine's cores, compute domains, and memory domains - the root every placement needs.
fu_capability_place_threads_by_affinity_k1 << 8Bind a thread to a set of cores, choosing where it runs.
fu_capability_place_threads_by_core_class_k1 << 9Steer a thread onto a class of core at creation, choosing where it runs.
fu_capability_reschedule_threads_by_class_k1 << 10Reclass a thread's scheduler to sleep or wake it, choosing when it runs.
fu_capability_place_memory_on_domain_k1 << 11Place a buffer's pages on a chosen memory domain.
fu_capability_place_huge_pages_on_domain_k1 << 12Place larger-than-base pages on a chosen memory domain, a narrower case of memory placement.
fu_capability_huge_transparent_pages_k1 << 13The kernel promotes base pages to huge pages on its own: passive, runtime-only.
fu_capability_colocate_pools_on_domain_k1 << 14The domain-aware colocated_pool and distributed_pool are compiled.
fu_capability_x86_cldemote_k1 << 15CLDEMOTE moves a just-written line from this core's private caches toward the shared LLC and retains it there.
fu_capability_arm64_dc_cvac_k1 << 16DC CVAC cleans a dirty line to the coherency point, AArch64's nearest demote.
fu_capability_riscv64_zicbom_k1 << 17The kernel enabled user-mode Zicbom cache-block management, attested through hwprobe - the C ABI's cascade selects cbo.clean on this bit.
fu_capability_x86_cmpccxadd_k1 << 18CMPCCXADD, the conditional atomic add - CPUID leaf 7, subleaf 1, EAX bit 7, on Sierra Forest, Diamond Rapids and their successors: a bounded claim as one instruction instead of a compare-exchange loop.
fu_capability_x86_raoint_k1 << 19RAO-INT aadd, aand, aor and axor - CPUID leaf 7, subleaf 1, EAX bit 3, on Grand Ridge:
fu_capability_arm64_lse_k1 << 20Armv8.1 FEAT_LSE: swp, cas, ldadd and the no-return st* forms, one instruction per read-modify-write instead of a load-exclusive loop.
fu_capability_arm64_rcpc_k1 << 21Armv8.3 FEAT_LRCPC: ldapr, the RCpc acquiring load that needn't wait for the core's earlier release stores.
fu_capability_riscv64_zacas_k1 << 22Zacas: amocas, compare-and-swap as one instruction instead of an lr and sc loop, attested by the kernel's hwprobe.
fu_capability_riscv64_atomic_k1 << 23The A extension: lr, sc and the amo* read-modify-writes, RISC-V's baseline every Linux ABI guarantees.
fu_capability_arm64_prfm_k1 << 24The AArch64 PRFM prefetch hints, base ISA that can never fault, so they are reported on every AArch64 core.
fu_capability_riscv64_zicbop_k1 << 25The Zicbop prefetches, prefetch.r and prefetch.w, encoded as ORI hints every hart accepts, so they are reported on every RISC-V core.
fu_capability_riscv64_zabha_k1 << 26Zabha: the byte and halfword amo forms, and with Zacas the byte and halfword amocas, attested by the kernel's hwprobe.
fu_capability_any_yield_kfu_capability_x86_pause_k | fu_capability_x86_tpause_k | fu_capability_riscv64_pause_k | fu_capability_arm64_yield_k | fu_capability_arm64_wfet_k | fu_capability_riscv64_wrs_kComposite mask of every busy-wait waiter bit above, to enumerate the ones a machine offers in one intersection with fu_runtime_capabilities.
fu_capability_any_instruction_k(fu_capability_any_yield_k | fu_capability_x86_cldemote_k | fu_capability_arm64_dc_cvac_k | fu_capability_riscv64_zicbom_k | fu_capability_x86_cmpccxadd_k | fu_capability_x86_raoint_k | fu_capability_arm64_lse_k | fu_capability_arm64_rcpc_k | fu_capability_riscv64_zacas_k | fu_capability_riscv64_atomic_k | fu_capability_arm64_prfm_k | fu_capability_riscv64_zicbop_k | fu_capability_riscv64_zabha_k)Composite mask of every instruction-level bit: the waiters, cache hints and atomics a CPU class may narrow, leaving the OS, topology, placement and NUMA bits alone.
fu_capabilities_all_k~0All-ones allow-mask: pass to fu_pool_new to disable capability filtering.

See also

fu_generation_tToken identifying one dispatch on one pool; an odd number, or zero once refused.
forkunion.h:1344 ↗
typedef size_t fu_generation_t;

Every generation advances the pool's internal epoch by exactly two: once at dispatch and once when the last contributor finishes. Odd epochs are in-flight, even are idle.

Functions

fu_version_*× 3Returns the major version component of the ForkUnion library.
forkunion.h:104 ↗
int fu_version_major(void);
Symbol
fu_version_major
fu_version_minor
fu_version_patch
fu_status_to_stringStatic, English description of status.
forkunion.h:186 ↗
char const * fu_status_to_string(fu_status_t status);

Never returns NULL, never allocates.

Parameters

statusinAny value; one this build does not name reads as the unknown text.
fu_comptime_capabilitiesWhich kernel facilities this build of ForkUnion was compiled to use.
forkunion.h:472 ↗
fu_capabilities_t fu_comptime_capabilities(void);

See also

fu_runtime_capabilitiesWhich features this machine turned out to offer, probing the CPU and the memory system.
forkunion.h:478 ↗
fu_capabilities_t fu_runtime_capabilities(void);

See also

fu_name_capabilitiesWrites capabilities as a comma-separated name list such as "arm64_yield,arm64_wfet".
forkunion.h:490 ↗
fu_status_t fu_name_capabilities(
    fu_capabilities_t capabilities,
    char *name_buffer,
    size_t name_buffer_length,
    size_t *written_out);

Parameters

capabilitiesinThe features to name.
name_bufferoutDestination, always null-terminated; the list is truncated to fit.
name_buffer_lengthinSize of name_buffer in bytes.
written_outoutReceives the bytes written, excluding the null terminator.

Statuses

fu_pool_newCreates an empty pool constrained to an allow-mask of capabilities.
forkunion.h:888 ↗
fu_status_t fu_pool_new(
    char const *name,
    fu_capabilities_t allowed,
    fu_pool_t *pool_out);

The waiter, and whether a spawn spans domains, come from fu_runtime_capabilities() & allowed, so clearing bits forces a lesser waiter or the flat non-NUMA pool - handy for benchmarking. Creation takes no topology: placement is decided at spawn, where you hand over the fu_topology_t. The pool starts empty; call fu_pool_spawn before use.

Parameters

nameinOptional pool name, may be NULL. Truncated to what the platform's thread naming accepts rather than refused - the name exists to tell one pool's threads from another's in a debugger, and a shortened name still does that.
allowedinAllow-mask of fu_capabilities_t; fu_capabilities_all_k skips filtering.
pool_outoutReceives the opaque pool handle.

Notes

Thread-safe.

See also

Statuses

fu_pool_deleteDestroys a pool and frees its resources; the handle is invalid afterward.
forkunion.h:896 ↗
void fu_pool_delete(fu_pool_t pool);

Parameters

poolinPool handle, may be NULL - a no-op. Pending tasks must have completed.

Notes

Must not run concurrently with other operations on pool.

See also

fu_pool_terminate to stop workers but keep the handle.

fu_pool_capabilitiesThe capabilities an initialized pool actually uses - a subset of comptime & runtime.
forkunion.h:903 ↗
fu_status_t fu_pool_capabilities(
    fu_pool_t pool,
    fu_capabilities_t *capabilities_out);

Parameters

poolinPool handle, must not be NULL.
capabilities_outoutReceives the capabilities the pool runs with.
fu_pool_spawnSpawns threads workers across the whole machine, readying the pool for dispatch.
forkunion.h:926 ↗
fu_status_t fu_pool_spawn(
    fu_topology_t topology,
    fu_pool_t pool,
    size_t threads,
    fu_caller_exclusivity_t exclusivity);

Covers every compute domain. If a prior fu_pool_spawn_on left the pool pinned to a single domain, this rebuilds it as a whole-machine pool before spawning. The call returns once every worker has started.

Parameters

topologyinMachine topology from fu_topology_new; read only during this call, so it may be freed as soon as this returns.
poolinPool handle, must not be NULL.
threadsinWorker count, must be > 0; an inclusive pool counts the caller as one.
exclusivityinWhether the calling thread also executes tasks.

Notes

Not thread-safe; call once per pool, or again after fu_pool_terminate.

See also

fu_pool_spawn_on to pin to one compute domain, fu_logical_cores_count for the count.

Statuses

fu_pool_spawn_onSpawns threads workers pinned to a single compute domain.
forkunion.h:951 ↗
fu_status_t fu_pool_spawn_on(
    fu_topology_t topology,
    fu_pool_t pool,
    size_t compute_domain_index,
    size_t threads,
    fu_caller_exclusivity_t exclusivity);

Placement lives here, not in creation: fu_pool_new allocates the handle, and this binds it to a single compute domain, so its workers and their memory-domain-local allocations stay on that domain. Spawn one pool per compute domain and coordinate them with the generation-token API; fu_pool_spawn instead spans all compute domains. Without NUMA, only compute domain 0 is valid. It returns once every worker started pinned to compute_domain_index.

Parameters

topologyinMachine topology from fu_topology_new; read only during this call.
poolinPool handle, must not be NULL.
compute_domain_indexinTarget compute domain, in [0, fu_compute_domains_count()).
threadsinWorker count, must be > 0.
exclusivityinWhether the calling thread also executes tasks.

Notes

Not thread-safe; call once per pool.

See also

Statuses

fu_pool_caller_exclusivityWhether the calling thread executes a slice of each dispatch.
forkunion.h:999 ↗
fu_status_t fu_pool_caller_exclusivity(
    fu_pool_t pool,
    fu_caller_exclusivity_t *exclusivity_out);

On inclusive pools the caller's slice runs only inside fu_pool_unsafe_join, so fu_pool_is_complete cannot be reached by polling alone.

Parameters

poolinPool handle, must not be NULL and initialized.
exclusivity_outoutReceives whether the caller runs a slice.

Notes

Not synchronized. Reflects the most recent fu_pool_spawn, so it survives a re-spawn.

See also

Statuses

fu_pool_threads_count_inThe worker count in one compute domain of the pool.
forkunion.h:1030 ↗
fu_status_t fu_pool_threads_count_in(
    fu_pool_t pool,
    size_t compute_domain_index,
    size_t *threads_out);

Parameters

poolinPool handle, must not be NULL.
compute_domain_indexinIn [0, fu_pool_compute_domains_count(pool)), or the call returns fu_invalid_argument_k.
threads_outoutReceives the number of threads in that domain.

Notes

Not synchronized.

See also

Statuses

fu_pool_threads_countThe total worker count, including the caller on an inclusive pool.
forkunion.h:1044 ↗
fu_status_t fu_pool_threads_count(fu_pool_t pool, size_t *threads_out);

The count is the one fu_pool_spawn received.

Parameters

poolinPool handle, must not be NULL.
threads_outoutReceives the number of threads.

Notes

Not synchronized.

Statuses

fu_pool_locate_thread_inConverts a global thread index to a local thread index within a compute_domain.
forkunion.h:1059 ↗
fu_status_t fu_pool_locate_thread_in(
    fu_pool_t pool,
    size_t global_thread_index,
    size_t compute_domain_index,
    size_t *local_index_out);

Parameters

poolinThread pool handle, must not be NULL.
global_thread_indexinThe global thread index to convert.
compute_domain_indexinIndex of the compute_domain, must be < fu_pool_compute_domains_count(pool), else fu_invalid_argument_k.
local_index_outoutReceives the local thread index within the specified compute_domain.

Statuses

fu_pool_napParks idle workers on the CPU's address monitor, for short idle periods.
forkunion.h:1071 ↗
void fu_pool_nap(fu_pool_t pool);

Workers stay on their cores and wake with no syscall of their own. Without a monitor they keep spinning on a hint. With enough privileges on Linux and FreeBSD they also drop to the idle scheduling class, and on Linux under schedutil to a lower frequency.

Parameters

poolinPool handle, must not be NULL.

Notes

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

fu_pool_sleepBlocks idle workers in the kernel, for long idle periods.
forkunion.h:1083 ↗
void fu_pool_sleep(fu_pool_t pool, size_t micros);

Workers leave their cores, at the price of syscalls in the next dispatch. Same as fu_pool_nap where the kernel can't wait on a word, which is Apple's case.

Parameters

poolinPool handle, must not be NULL.
microsinLongest a worker stays blocked before re-checking, or 0 to leave the wake to the next dispatch alone.

Notes

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

fu_pool_terminateStops the workers and clears the pool, keeping the handle for a re-spawn.
forkunion.h:1095 ↗
void fu_pool_terminate(fu_pool_t pool);

Joins every worker and releases their resources; call fu_pool_spawn again, e.g. to change the thread count. Use fu_pool_delete for permanent teardown.

Parameters

poolinPool handle, must not be NULL.

Notes

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

See also

fu_pool_for_threadsRuns callback once on every worker, blocking until all return - OpenMP's parallel.
forkunion.h:1250 ↗
fu_status_t fu_pool_for_threads(
    fu_pool_t pool,
    fu_for_threads_t callback,
    fu_lambda_context_t context);

The callback receives context, its thread index in [0, threads_count), and the compute_domain - the NUMA node and QoS level - it runs on.

void hello(void *ctx, size_t thread, size_t compute_domain) {
    printf("Hello from thread %zu on compute_domain %zu\n", thread, compute_domain);
}
if (fu_pool_for_threads(pool, hello, NULL) != fu_success_k) return; // ! Never spawned, or busy

Parameters

poolinPool handle, must not be NULL and initialized.
callbackinRuns once per worker, must not be NULL.
contextinShared context passed to every call, may be NULL.

Notes

Returns only after every worker completes; no extra synchronization is needed.

See also

fu_pool_unsafe_for_threads for the non-blocking form.

Statuses

fu_pool_for_slicesSplits [0, n) into contiguous slices, one per worker, and blocks until all finish.
forkunion.h:1278 ↗
fu_status_t fu_pool_for_slices(
    fu_pool_t pool,
    size_t n,
    fu_for_range_t callback,
    fu_lambda_context_t context);

One contiguous range per worker - ideal for vectorized or per-slice-setup work where sequential access matters, since each worker touches its data in order and only sets up once. Every worker is called exactly once, an idle one with count == 0. The callback receives context, the run's first index, count of tasks, thread index, and compute_domain.

void normalize(void *array, size_t first, size_t count, size_t thread, size_t compute_domain) {
    float *data = (float *)array;
    for (size_t i = first; i != first + count; ++i) data[i] = sqrtf(data[i]); // One sequential sweep
}
if (fu_pool_for_slices(pool, length, normalize, values) != fu_success_k) return; // ! Never spawned, or busy

Parameters

poolinPool handle, must not be NULL and initialized.
ninTotal tasks, may be 0 - a no-op.
callbackinRuns once per slice, must not be NULL if n > 0.
contextinShared context, may be NULL.

See also

fu_pool_for_n for per-index dispatch.

Statuses

fu_pool_for_nRuns callback for each of n similar-cost tasks, blocking until done.
forkunion.h:1305 ↗
fu_status_t fu_pool_for_n(
    fu_pool_t pool,
    size_t n,
    fu_for_task_t callback,
    fu_lambda_context_t context);

Tasks split into equal contiguous chunks per worker, keeping coordination near zero - the right choice when tasks cost about the same, so a static split stays balanced. The callback receives context, the task index in [0, n), the thread index, and compute_domain.

void double_each(void *array, size_t i, size_t thread, size_t compute_domain) {
    int *data = (int *)array;
    data[i] = data[i] * 2; // Uniform O(1) work per index
}
if (fu_pool_for_n(pool, length, double_each, array) != fu_success_k) return; // ! Never spawned, or busy

Parameters

poolinPool handle, must not be NULL and initialized.
ninTask count, may be 0 - a no-op.
callbackinRuns once per task, must not be NULL if n > 0.
contextinShared context, may be NULL.

See also

fu_pool_for_n_dynamic for uneven workloads, fu_pool_for_slices for range callbacks.

Statuses

fu_pool_for_n_dynamicRuns callback per n uneven-cost tasks via work-stealing, blocking until done.
forkunion.h:1332 ↗
fu_status_t fu_pool_for_n_dynamic(
    fu_pool_t pool,
    size_t n,
    fu_for_task_t callback,
    fu_lambda_context_t context);

Each worker starts with one task, then claims more from a shared cursor as it finishes, so fast workers absorb the slack and one slow task never stalls the rest - the right choice when task cost is unpredictable or data-dependent. Same callback shape as fu_pool_for_n, but task order is not deterministic.

void variable_work(void *ctx, size_t task, size_t thread, size_t compute_domain) {
    complex_computation(task); // May take 1us or 1ms
}
if (fu_pool_for_n_dynamic(pool, count, variable_work, ctx) != fu_success_k) return; // ! Never spawned, or busy

Parameters

poolinPool handle, must not be NULL and initialized.
ninTask count, may be 0 - a no-op.
callbackinRuns once per task, must not be NULL if n > 0.
contextinShared context, may be NULL.

See also

fu_pool_for_n for balanced workloads.

Statuses

fu_pool_unsafe_for_threadsDispatches callback on every worker without blocking; returns a generation token.
forkunion.h:1370 ↗
fu_status_t fu_pool_unsafe_for_threads(
    fu_pool_t pool,
    fu_for_threads_t callback,
    fu_lambda_context_t context,
    fu_generation_t *generation_out);

On inclusive pools the caller's own slice runs inside fu_pool_unsafe_join, so the pool cannot complete until the caller joins.

fu_generation_t generation;
if (fu_pool_unsafe_for_threads(pool, callback, context, &generation) != fu_success_k) return;
prepare_next_batch();
fu_pool_unsafe_join(pool, generation);

Parameters

poolinPool handle, must not be NULL and initialized.
callbackinRuns on each thread, must not be NULL.
contextinShared context, must stay valid until the join, may be NULL.
generation_outoutReceives the token for fu_pool_unsafe_join.

Notes

Returns immediately. Call no other pool operation until you join this generation.

See also

Statuses

  • fu_contended_k With the zero token while the pool runs another dispatch, and then nothing ran.
fu_pool_is_completeWhether generation has finished - a non-blocking poll.
forkunion.h:1382 ↗
fu_status_t fu_pool_is_complete(
    fu_pool_t pool,
    fu_generation_t generation,
    fu_bool_t *complete_out);

Parameters

poolinPool handle, must not be NULL and initialized.
generationinToken from fu_pool_unsafe_for_threads.
complete_outoutReceives non-zero once complete, which also publishes every contributor's writes.

Notes

On inclusive pools this turns non-zero only after fu_pool_unsafe_join runs the caller's slice, so poll-then-join is for exclusive pools only.

fu_pool_unsafe_joinBlocks until generation completes, running the caller's slice on an inclusive pool.
forkunion.h:1404 ↗
void fu_pool_unsafe_join(fu_pool_t pool, fu_generation_t generation);

The synchronization point for the non-blocking API: on return every contributor is done, their writes are visible, and the pool is ready for the next dispatch. Idempotent on a stale or already-joined generation.

fu_generation_t generation;
fu_bool_t complete = 0;
if (fu_pool_unsafe_for_threads(pool, callback, context, &generation) != fu_success_k) return;
while (!complete && fu_pool_is_complete(pool, generation, &complete) == fu_success_k)
    do_other_work(); // ! Exclusive pools only
fu_pool_unsafe_join(pool, generation);

Parameters

poolinPool handle, must not be NULL and initialized.
generationinToken from fu_pool_unsafe_for_threads.

See also

Macros

FORKUNION_POOL_NAME_CAPACITYCapacity of a pool name including its null terminator; a longer name is clipped to fit.
forkunion.h:189 ↗
#define FORKUNION_POOL_NAME_CAPACITY 16
FORKUNION_CAPABILITIES_NAME_CAPACITYBuffer size fu_name_capabilities never overruns, including its null terminator.
forkunion.h:192 ↗
#define FORKUNION_CAPABILITIES_NAME_CAPACITY 512