xtc_blocking(3)

---

xtc_blocking(3)

offload blocking work to a thread pool without stalling the loop

XTC_BLOCKING(3) Library Functions Manual XTC_BLOCKING(3)

xtc_blocking_run, xtc_blocking_run_off_loop, xtc_blocking_submit, xtc_blocking_pool_size, xtc_blocking_shutdown — offload blocking work to a thread pool without stalling the loop

#include <xtc.h>
#include <xtc_blocking.h>

int
xtc_blocking_run(int (*fn)(void *), void *arg, int *out_result);

int
xtc_blocking_run_off_loop(int (*fn)(void *), void *arg, int *out_result);

int
xtc_blocking_submit(int (*fn)(void *), void *arg);

int
xtc_blocking_pool_size(int nthreads);

void
xtc_blocking_shutdown(void);

A loop thread must never block in a syscall: doing so stalls every other process sharing that loop. Some work is unavoidably blocking however -- reads and fsync(2) on regular files (which are not pollable), getaddrinfo(3), and third-party libraries. The xtc_blocking facility runs such a call on a dedicated pool thread and parks the calling process until it finishes, so the loop keeps running other work meanwhile. The pool thread wakes the parked process through its task waker when the call finishes. No file descriptor is used per call, so offloading keeps working when the process is out of descriptors.

() runs fn(arg) on a pool thread and parks the caller until it completes, storing fn's return value in *out_result. It must be called from within a process or coroutine running on a loop; called outside that context it runs fn synchronously on the current thread -- always correct, just not yielding. On a loop it never runs fn on the loop thread: if the offload cannot be queued it returns an error without running fn (see RETURN VALUES).

() is the counterpart for a caller that is a plain OS thread and not a fiber (its () is none): it offloads fn(arg) to the pool and blocks the calling thread until it completes, rather than parking a fiber (which a bare thread cannot do), so fn runs on a pool worker instead of inline on the caller. It remains synchronous from the caller's point of view -- it blocks the calling thread until fn completes -- and it does not shorten any lock the caller holds across the call: moving a fsync(2) off the caller thread does not shorten a lock held around it. It is a building block for a group-commit or batching design (submit once, wait once, wake many), not a way to let a non-fiber caller do other work during the call: to keep serving other multiplexed tasks during a blocking call, make those tasks fibers on a loop and use xtc_blocking_run(), which yields. It returns XTC_E_INVAL if fn is NULL or if it is called from a fiber (use xtc_blocking_run() there), and the queue errors listed under RETURN VALUES.

() is the fire-and-forget variant: it hands fn(arg) to the pool and returns immediately without waiting for or collecting the result. It never parks, so it is callable from any context (for example prefetch or read-ahead). The caller owns arg's lifetime until fn runs (or has fn free it); there is no completion signal.

() pins the pool to a fixed nthreads worker threads, overriding the automatic default. It must be called before the first xtc_blocking_run() or xtc_blocking_submit(); a later call returns XTC_E_INVAL (too late). By default the pool auto-sizes: it starts with a CPU-scaled worker count ((4, , online CPUs), capped at 64) and grows on demand up to 64 when work queues faster than idle workers can take it, so the offload path is neither an artificial bottleneck on a large host nor over-provisioned on a small one. An explicit size disables growth and fixes the pool at exactly that many threads.

() stops the pool, joining its threads. It is idempotent, for orderly shutdown and leak-checked test runs. A new xtc_blocking_run() after shutdown restarts the pool.

xtc_blocking_run() and xtc_blocking_run_off_loop() return XTC_OK once fn has run, with its result in *out_result. xtc_blocking_submit() returns XTC_OK if the work was enqueued. All three return, without running fn:

fn is NULL (or, for xtc_blocking_run_off_loop(), the caller is a fiber).
The offload queue already holds 65536 waiting calls. The queue is bounded so that a burst the pool cannot drain becomes an error the caller sees instead of unbounded backlog; back off and retry, or shed the request.
No pool thread could be started.
(xtc_blocking_submit() only) the work item could not be allocated.

Through 1.55.0 each call used a pipe for its wakeup (two descriptors per concurrent call) and, when pipe(2) failed, ran fn inline on the loop thread, stalling every other process on that loop. xtc_blocking_pool_size() returns XTC_OK, or XTC_E_INVAL if called after the pool has started.

xtc_proc(3), xtc_aio(3), xtc_async(3)

The XTC Project.

July 3, 2026 Debian

View the mdoc source