xtc_exec(3)
---xtc_exec(3)
L2 multi-loop work-stealing executor
| XTC_EXEC(3) | Library Functions Manual | XTC_EXEC(3) |
NAME
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_on — L2
multi-loop work-stealing executor
SYNOPSIS
#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);
DESCRIPTION
xtc_exec manages
N 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.
LIFECYCLE
xtc_exec_init()
constructs an executor with n_loops loops;
non-positive values default to a small reasonable count.
xtc_exec_fini()
joins every worker, frees every loop.
xtc_exec_run()
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
xtc_exec_stop()
is called from any thread.
xtc_exec_set_service_mode()
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.
xtc_exec_set_preempt()
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.
SPAWN PLACEMENT
xtc_exec_spawn()
puts the new task on a loop chosen by a round-robin counter.
xtc_exec_spawn_on()
forces placement on the given loop. The coroutine variants
xtc_exec_async()
and
xtc_exec_async_on()
mirror the contract.
Spawn calls are safe from any thread,
including before
xtc_exec_run():
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.
CROSS-THREAD WAKERS
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
xtc_io_wakeup()
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.
THREAD AFFINITY
xtc_exec_loop_id()
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.
xtc_shard_id()
and
xtc_shard_count()
are the Seastar-style per-shard API: the 0-based index of the loop the
caller runs on, and the number of shards. Unlike
xtc_exec_loop_id()
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.
xtc_exec_loop_stats()
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
(xtc_exec_spawn_on())
and processes are pinned to their loop and are never stolen; only the
unpinned general task pool (xtc_exec_spawn())
migrates.
RETURN VALUES
XTC_OK on success;
XTC_E_INVAL on a bad argument;
XTC_E_NOMEM on allocation failure;
XTC_E_INTERNAL on platform failure.
THREAD SAFETY
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.
EXAMPLES
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;
}
SEE ALSO
HISTORY
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 __os_ncpus.
| May 25, 2026 | Debian |