xtc_exec(3)

---

xtc_exec(3)

L2 multi-loop work-stealing executor

XTC_EXEC(3) Library Functions Manual XTC_EXEC(3)

xtc_exec_init, xtc_exec_fini, xtc_exec_run, xtc_exec_set_service_mode, xtc_exec_set_preempt, xtc_exec_stop, xtc_exec_n_loops, xtc_exec_loop, xtc_exec_loop_stats, xtc_exec_loop_id, xtc_shard_id, xtc_shard_count, xtc_exec_spawn, xtc_exec_spawn_on, xtc_exec_async, xtc_exec_async_onL2 multi-loop work-stealing executor

#include <xtc_exec.h>

int
xtc_exec_init(xtc_exec_t **out, int n_loops);

int
xtc_exec_fini(xtc_exec_t *exec);

int
xtc_exec_run(xtc_exec_t *exec);

void
xtc_exec_set_service_mode(xtc_exec_t *exec, int on);

int
xtc_exec_set_preempt(xtc_exec_t *exec, int64_t interval_ns);

int
xtc_exec_stop(xtc_exec_t *exec);

int
xtc_exec_n_loops(xtc_exec_t *exec);

xtc_loop_t *
xtc_exec_loop(xtc_exec_t *exec, int idx);

int
xtc_exec_loop_stats(xtc_exec_t *exec, int idx, xtc_loop_stats_t *out);

int
xtc_exec_loop_id(void);

int
xtc_shard_id(void);

int
xtc_shard_count(void);

int
xtc_exec_spawn(xtc_exec_t *exec, xtc_task_fn fn, void *user, xtc_task_t **out);

int
xtc_exec_spawn_on(xtc_exec_t *exec, int idx, xtc_task_fn fn, void *user, xtc_task_t **out);

int
xtc_exec_async(xtc_exec_t *exec, xtc_coro_fn fn, void *arg, xtc_task_t **out);

int
xtc_exec_async_on(xtc_exec_t *exec, int idx, xtc_coro_fn fn, void *arg, xtc_task_t **out);

xtc_exec manages event loops, one per worker thread. Each loop owns a Chase-Lev work-stealing deque as its run queue plus an MPSC inbox for cross-thread wakers and remote spawns.

() constructs an executor with n_loops loops; non-positive values default to a small reasonable count. () joins every worker, frees every loop.

() spawns n_loops worker threads (one per loop) and blocks on the calling thread until the executor reaches a quiescent state 2014 every spawned task DONE, every timer fired or cancelled, every inbox drained, every deque empty 2014 or until () is called from any thread.

() opts the executor out of that quiescent-state auto-stop. When on is non-zero, xtc_exec_run() blocks until xtc_exec_stop() is called explicitly and never stops merely because every loop went idle. Use it for a long-running service (a supervised xtc_app(3)), where a transient all-idle window during startup -- before every supervised child has been scheduled across loops -- must not terminate the application. A bare work-pool leaves it off so the executor drains and returns when its tasks finish.

() enables cooperative-assisted preemption: each worker arms a per-worker CPU-time interval timer at interval_ns (of that worker thread's own CPU time), and a timer tick makes that worker's xtc_yield_if_due(3) callers yield, so a long compute fiber that periodically consults a yield check is time-sliced without a manual budget. interval_ns of 0 disables it (the default). Call before xtc_exec_run(); workers read the setting at start. Returns XTC_E_NOSYS where the platform lacks per-thread CPU-time timers (the setting is stored but arming is a no-op). This does not preempt a fiber that never reaches a yield check.

Workers steal from peers when their own deque empties before going to xtc_io(3) poll. M5 uses a random victim policy; NUMA-near steal ordering arrives in M5.5.

() puts the new task on a loop chosen by a round-robin counter. () forces placement on the given loop. The coroutine variants () and () mirror the contract.

Spawn calls are safe from any thread, including before (): work queued in advance is processed when the workers start. Spawns from a non-owner thread are routed through the target loop's inbox and surface on the loop's next step.

A xtc_waker_t captured on one loop may be fired from any thread; cross-thread fires are routed through the target loop's inbox plus an () ping that breaks the target out of xtc_io(3) poll. The wake remains idempotent: many concurrent wakes collapse to at most one re-queue per yielded run.

() returns the executor index of the loop the calling task is running on, or -1 from a thread that is not a worker. Tests use this to verify spawn placement and steal behaviour.

() and () are the Seastar-style per-shard API: the 0-based index of the loop the caller runs on, and the number of shards. Unlike () they treat a standalone loop (not under an executor) as shard 0 of 1, so a shared-nothing consumer can always index per-core state with xtc_shard_id() regardless of whether it runs under the executor or a single loop. Both return the off-loop sentinels (-1 and 0) only when the caller is not on any loop.

() fills out with loop idx's work counters: tasks_run (task steps executed on that loop) and steals (tasks that loop stole from a peer). The values are lock-free snapshots, for load-balance and tail-latency diagnosis. Note that explicitly placed work (()) and processes are pinned to their loop and are never stolen; only the unpinned general task pool (xtc_exec_spawn()) migrates.

XTC_OK on success; XTC_E_INVAL on a bad argument; XTC_E_NOMEM on allocation failure; XTC_E_INTERNAL on platform failure.

All public functions are safe to call from any thread. A given task handle (out_task) returned from a spawn must be used only in ways consistent with the M3/M4 task contracts; in particular, a xtc_waker_t captured from one task may travel anywhere.

static intptr_t hello(void *arg) {
        printf("hello from loop %d\n", xtc_exec_loop_id());
        return 0;
}

int main(void) {
        xtc_exec_t *e;
        xtc_exec_init(&e, 4);
        for (int i = 0; i < 4; i++)
                xtc_exec_async(e, hello, NULL, NULL);
        xtc_exec_run(e);
        xtc_exec_fini(e);
        return 0;
}

xtc_loop(3), xtc_async(3), xtc_io(3), xtc(7)

First appeared in xtc 0.0.1 (M5). M5.5 will add NUMA-aware steal ordering and configure-time auto-detect of CPU count via .

May 25, 2026 Debian

View the mdoc source