typedef int fu_bool_t; 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:
spawnto initialize worker threads, andfor_threadsto 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_tis backed bylinux_numa_allocator_t,freebsd_numa_allocator_t, orwindows_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.
fu_status_tWhy a call failed, in a vocabulary the core and every binding share.
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_k | 0 | The call completed and any output pointer holds a meaningful value. |
|---|---|---|
fu_unknown_k | -1 | No reason was reported, or one this build does not name. |
fu_bad_alloc_k | -2 | An allocation or mapping failed; a smaller request may succeed. |
fu_capacity_exhausted_k | -3 | A fixed ceiling was reached, so a smaller request will not help either. |
fu_invalid_argument_k | -4 | An argument was malformed, out of range, or would overflow a byte count. |
fu_config_mismatch_k | -5 | The handles or the pool kind cannot serve this call together. |
fu_already_spawned_k | -6 | The pool is already spawned; terminate it first. |
fu_not_spawned_k | -7 | The pool holds no workers: it was never spawned, or was terminated since. |
fu_thread_refused_k | -8 | The OS declined to create a thread - a resource limit, or permissions. |
fu_topology_unavailable_k | -9 | The machine could not be described; transient if its CPU set changed mid-probe. |
fu_permission_denied_k | -10 | A privileged operation was declined. |
fu_unsupported_k | -11 | This build or this machine has no such facility. |
fu_library_missing_k | -12 | Binding-only: nothing to load, or a different major version. |
fu_symbol_missing_k | -13 | Binding-only: the loaded core is missing a symbol. |
fu_contended_k | -14 | The 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.
typedef void * fu_pool_t; fu_lambda_context_tType-punned pointer to a user-defined callback context.
typedef void * fu_lambda_context_t; fu_for_threads_tCallback type for thread-level operations.
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.
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.
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.
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_k | The calling thread participates in the workload. | |
|---|---|---|
fu_caller_exclusive_k | The 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.
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_k | 0 | No bit set: nothing requested, and nothing yet found. |
|---|---|---|
fu_capability_x86_pause_k | 1 << 0 | The 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_k | 1 << 1 | TPAUSE 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_k | 1 << 2 | The YIELD hint, on every AArch64: releases the pipeline to a sibling hardware thread and costs nothing where there is none. |
fu_capability_arm64_wfet_k | 1 << 3 | WFET 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_k | 1 << 4 | The 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_k | 1 << 5 | WRS.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_k | 1 << 6 | Own the raw OS thread handle instead of a std::thread - the substrate every thread lever below stands on. |
fu_capability_topology_k | 1 << 7 | Enumerate this machine's cores, compute domains, and memory domains - the root every placement needs. |
fu_capability_place_threads_by_affinity_k | 1 << 8 | Bind a thread to a set of cores, choosing where it runs. |
fu_capability_place_threads_by_core_class_k | 1 << 9 | Steer a thread onto a class of core at creation, choosing where it runs. |
fu_capability_reschedule_threads_by_class_k | 1 << 10 | Reclass a thread's scheduler to sleep or wake it, choosing when it runs. |
fu_capability_place_memory_on_domain_k | 1 << 11 | Place a buffer's pages on a chosen memory domain. |
fu_capability_place_huge_pages_on_domain_k | 1 << 12 | Place larger-than-base pages on a chosen memory domain, a narrower case of memory placement. |
fu_capability_huge_transparent_pages_k | 1 << 13 | The kernel promotes base pages to huge pages on its own: passive, runtime-only. |
fu_capability_colocate_pools_on_domain_k | 1 << 14 | The domain-aware colocated_pool and distributed_pool are compiled. |
fu_capability_x86_cldemote_k | 1 << 15 | CLDEMOTE moves a just-written line from this core's private caches toward the shared LLC and retains it there. |
fu_capability_arm64_dc_cvac_k | 1 << 16 | DC CVAC cleans a dirty line to the coherency point, AArch64's nearest demote. |
fu_capability_riscv64_zicbom_k | 1 << 17 | The 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_k | 1 << 18 | CMPCCXADD, 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_k | 1 << 19 | RAO-INT aadd, aand, aor and axor - CPUID leaf 7, subleaf 1, EAX bit 3, on Grand Ridge: |
fu_capability_arm64_lse_k | 1 << 20 | Armv8.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_k | 1 << 21 | Armv8.3 FEAT_LRCPC: ldapr, the RCpc acquiring load that needn't wait for the core's earlier release stores. |
fu_capability_riscv64_zacas_k | 1 << 22 | Zacas: amocas, compare-and-swap as one instruction instead of an lr and sc loop, attested by the kernel's hwprobe. |
fu_capability_riscv64_atomic_k | 1 << 23 | The A extension: lr, sc and the amo* read-modify-writes, RISC-V's baseline every Linux ABI guarantees. |
fu_capability_arm64_prfm_k | 1 << 24 | The AArch64 PRFM prefetch hints, base ISA that can never fault, so they are reported on every AArch64 core. |
fu_capability_riscv64_zicbop_k | 1 << 25 | The 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_k | 1 << 26 | Zabha: the byte and halfword amo forms, and with Zacas the byte and halfword amocas, attested by the kernel's hwprobe. |
fu_capability_any_yield_k | fu_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_k | Composite 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 | ~0 | All-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.
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.
int fu_version_major(void); | Symbol |
|---|
fu_version_major |
fu_version_minor |
fu_version_patch |
fu_status_to_stringStatic, English description of status.
char const * fu_status_to_string(fu_status_t status); Never returns NULL, never allocates.
Parameters
status | in | Any 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.
fu_runtime_capabilitiesWhich features this machine turned out to offer, probing the CPU and the memory system.
fu_name_capabilitiesWrites capabilities as a comma-separated name list such as "arm64_yield,arm64_wfet".
fu_status_t fu_name_capabilities(
fu_capabilities_t capabilities,
char *name_buffer,
size_t name_buffer_length,
size_t *written_out); Parameters
capabilities | in | The features to name. |
|---|---|---|
name_buffer | out | Destination, always null-terminated; the list is truncated to fit. |
name_buffer_length | in | Size of name_buffer in bytes. |
written_out | out | Receives the bytes written, excluding the null terminator. |
Statuses
fu_invalid_argument_kA NULLwritten_out, a NULLname_bufferor an empty one.
fu_pool_newCreates an empty pool constrained to an allow-mask of capabilities.
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
name | in | Optional 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. |
|---|---|---|
allowed | in | Allow-mask of fu_capabilities_t; fu_capabilities_all_k skips filtering. |
pool_out | out | Receives the opaque pool handle. |
Notes
Thread-safe.
See also
Statuses
fu_invalid_argument_kA NULLpool_out.
fu_pool_deleteDestroys a pool and frees its resources; the handle is invalid afterward.
void fu_pool_delete(fu_pool_t pool); Parameters
pool | in | Pool 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.
fu_status_t fu_pool_capabilities(
fu_pool_t pool,
fu_capabilities_t *capabilities_out); Parameters
pool | in | Pool handle, must not be NULL. |
|---|---|---|
capabilities_out | out | Receives the capabilities the pool runs with. |
fu_pool_spawnSpawns threads workers across the whole machine, readying the pool for dispatch.
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
topology | in | Machine topology from fu_topology_new; read only during this call, so it may be freed as soon as this returns. |
|---|---|---|
pool | in | Pool handle, must not be NULL. |
threads | in | Worker count, must be > 0; an inclusive pool counts the caller as one. |
exclusivity | in | Whether 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_invalid_argument_kA NULL handle, an unnamed exclusivity, orthreadsof 0.fu_already_spawned_kWhile the pool still holds workers.fu_unsupported_kA caller-exclusive or multi-thread pool when OS threads are unavailable in this build.
fu_pool_spawn_onSpawns threads workers pinned to a single compute domain.
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
topology | in | Machine topology from fu_topology_new; read only during this call. |
|---|---|---|
pool | in | Pool handle, must not be NULL. |
compute_domain_index | in | Target compute domain, in [0, fu_compute_domains_count()). |
threads | in | Worker count, must be > 0. |
exclusivity | in | Whether the calling thread also executes tasks. |
Notes
Not thread-safe; call once per pool.
See also
Statuses
fu_invalid_argument_kA NULL handle, an unnamed exclusivity,threadsof 0, or acompute_domain_indexat or pastfu_compute_domains_count.fu_already_spawned_kWhile the pool still holds workers.fu_unsupported_kA caller-exclusive or multi-thread pool in a build without OS threads.
fu_pool_caller_exclusivityWhether the calling thread executes a slice of each dispatch.
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
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
exclusivity_out | out | Receives 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_invalid_argument_kA NULL handle or a NULLexclusivity_out.fu_not_spawned_kA pool with no workers.
fu_pool_threads_count_inThe worker count in one compute domain of the pool.
fu_status_t fu_pool_threads_count_in(
fu_pool_t pool,
size_t compute_domain_index,
size_t *threads_out); Parameters
pool | in | Pool handle, must not be NULL. |
|---|---|---|
compute_domain_index | in | In [0, fu_pool_compute_domains_count(pool)), or the call returns fu_invalid_argument_k. |
threads_out | out | Receives the number of threads in that domain. |
Notes
Not synchronized.
See also
Statuses
fu_invalid_argument_kA NULL handle or a NULLthreads_out.fu_not_spawned_kA pool with no workers.
fu_pool_threads_countThe total worker count, including the caller on an inclusive pool.
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
pool | in | Pool handle, must not be NULL. |
|---|---|---|
threads_out | out | Receives the number of threads. |
Notes
Not synchronized.
Statuses
fu_invalid_argument_kA NULL handle or a NULLthreads_out.fu_not_spawned_kA pool with no workers.
fu_pool_locate_thread_inConverts a global thread index to a local thread index within a compute_domain.
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
pool | in | Thread pool handle, must not be NULL. |
|---|---|---|
global_thread_index | in | The global thread index to convert. |
compute_domain_index | in | Index of the compute_domain, must be < fu_pool_compute_domains_count(pool), else fu_invalid_argument_k. |
local_index_out | out | Receives the local thread index within the specified compute_domain. |
Statuses
fu_invalid_argument_kA NULL handle, a NULLlocal_index_outor a compute domain out of range.fu_not_spawned_kA pool with no workers.
fu_pool_napParks idle workers on the CPU's address monitor, for short idle periods.
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
pool | in | Pool 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.
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
pool | in | Pool handle, must not be NULL. |
|---|---|---|
micros | in | Longest 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.
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
pool | in | Pool 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.
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 busyParameters
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
callback | in | Runs once per worker, must not be NULL. |
context | in | Shared 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_not_spawned_kA pool with no workers.fu_contended_kWhile the pool runs another dispatch, and then nothing ran.
fu_pool_for_slicesSplits [0, n) into contiguous slices, one per worker, and blocks until all finish.
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 busyParameters
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
n | in | Total tasks, may be 0 - a no-op. |
callback | in | Runs once per slice, must not be NULL if n > 0. |
context | in | Shared context, may be NULL. |
See also
fu_pool_for_n for per-index dispatch.
Statuses
fu_not_spawned_kA pool with no workers.fu_contended_kWhile the pool runs another dispatch, and then nothing ran.
fu_pool_for_nRuns callback for each of n similar-cost tasks, blocking until done.
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 busyParameters
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
n | in | Task count, may be 0 - a no-op. |
callback | in | Runs once per task, must not be NULL if n > 0. |
context | in | Shared context, may be NULL. |
See also
fu_pool_for_n_dynamic for uneven workloads, fu_pool_for_slices for range callbacks.
Statuses
fu_not_spawned_kA pool with no workers.fu_contended_kWhile the pool runs another dispatch, and then nothing ran.
fu_pool_for_n_dynamicRuns callback per n uneven-cost tasks via work-stealing, blocking until done.
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 busyParameters
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
n | in | Task count, may be 0 - a no-op. |
callback | in | Runs once per task, must not be NULL if n > 0. |
context | in | Shared context, may be NULL. |
See also
fu_pool_for_n for balanced workloads.
Statuses
fu_not_spawned_kA pool with no workers.fu_contended_kWhile the pool runs another dispatch, and then nothing ran.
fu_pool_unsafe_for_threadsDispatches callback on every worker without blocking; returns a generation token.
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
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
callback | in | Runs on each thread, must not be NULL. |
context | in | Shared context, must stay valid until the join, may be NULL. |
generation_out | out | Receives the token for fu_pool_unsafe_join. |
Notes
Returns immediately. Call no other pool operation until you join this generation.
See also
fu_pool_unsafe_join, fu_pool_for_threads for the blocking form.
Statuses
fu_contended_kWith the zero token while the pool runs another dispatch, and then nothing ran.
fu_pool_is_completeWhether generation has finished - a non-blocking poll.
fu_status_t fu_pool_is_complete(
fu_pool_t pool,
fu_generation_t generation,
fu_bool_t *complete_out); Parameters
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
generation | in | Token from fu_pool_unsafe_for_threads. |
complete_out | out | Receives 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.
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
pool | in | Pool handle, must not be NULL and initialized. |
|---|---|---|
generation | in | Token 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.
#define FORKUNION_POOL_NAME_CAPACITY 16 FORKUNION_CAPABILITIES_NAME_CAPACITYBuffer size fu_name_capabilities never overruns, including its null terminator.
#define FORKUNION_CAPABILITIES_NAME_CAPACITY 512