xtc_loop(3)
---xtc_loop(3)
L2 single-thread event loop, tasks, wakers, timers
| XTC_LOOP(3) | Library Functions Manual | XTC_LOOP(3) |
NAME
xtc_loop_init,
xtc_loop_fini, xtc_loop_run,
xtc_loop_stop,
xtc_loop_wake, xtc_loop_res,
xtc_task_spawn,
xtc_task_waker,
xtc_task_park_on_timer,
xtc_task_park_on_fd,
xtc_waker_wake,
xtc_timer_set,
xtc_timer_cancel,
xtc_timer_release — L2
single-thread event loop, tasks, wakers, timers
SYNOPSIS
#include
<xtc_loop.h>
int
xtc_loop_init(xtc_loop_t
**out);
int
xtc_loop_fini(xtc_loop_t
*loop);
int
xtc_loop_run(xtc_loop_t
*loop);
int
xtc_loop_stop(xtc_loop_t
*loop);
int
xtc_loop_wake(xtc_loop_t
*loop);
struct xtc_res *
xtc_loop_res(xtc_loop_t
*loop);
int
xtc_task_spawn(xtc_loop_t
*loop, xtc_task_fn
fn, void *user,
xtc_task_t
**out_task);
int
xtc_task_waker(xtc_task_t
*task, xtc_waker_t
*out);
int
xtc_task_park_on_timer(xtc_task_t
*self, int64_t
delay_ns);
int
xtc_task_park_on_fd(xtc_task_t
*self, int fd,
uint32_t interest);
int
xtc_waker_wake(const
xtc_waker_t *w);
int
xtc_timer_set(xtc_loop_t
*loop, int64_t
delay_ns, xtc_timer_fn
fn, void *user,
xtc_timer_t **out);
int
xtc_timer_cancel(xtc_timer_t
*timer);
int
xtc_timer_release(xtc_timer_t
*timer);
DESCRIPTION
The L2 event loop drives a single OS thread. A loop owns one xtc_io_t, a FIFO run queue of ready tasks, and a min-heap of pending timers.
A task is a small struct carrying a function pointer, a state, and an opaque user pointer. The function is called by the loop whenever the task becomes runnable; its return value tells the loop what to do next:
XTC_TASK_DONE- The task is finished. A task created with
xtc_task_spawn() or xtc_async(3) is reaped atxtc_loop_fini(), so the handle it returned stays valid until then. The fiber of a process (xtc_proc(3)), whose task handle is never handed out, is reclaimed as soon as it finishes: its stack returns to a small per-thread cache (or is unmapped) and its task is recycled, so memory and mappings track the number of live processes, not the number ever spawned. XTC_TASK_RESCHED- Re-queue the task at the back of the run queue: it runs again only after every task already queued. This holds for pinned and migratable tasks alike; when a loop has both kinds ready it alternates between them. Releases up to 1.55.0 re-ran a yielding migratable task first, starving the other migratable tasks on its loop.
XTC_TASK_PENDING- The task is waiting on a wakeup; do not run it again until either its waker fires or the timer / fd it parked on becomes ready.
LIFECYCLE
xtc_loop_init()
allocates a fresh loop with no spawned tasks. Each loop owns a resource
accountant (xtc_res_t) with default caps; access it
via
xtc_loop_res().
xtc_loop_run()
runs until every spawned task has returned
XTC_TASK_DONE and every pending timer has fired or
been cancelled.
xtc_loop_stop()
flips a flag that causes the loop to return after the current task step
completes.
xtc_loop_wake()
nudges a loop's poller out of its I/O wait from any OS thread, so it
re-polls and re-checks runnability. It is the loop-level companion to
xtc_proc_wake():
when a foreign thread makes a condition true for a task parked on the loop
(writes an fd the loop watches, sets an embedder latch), it MUST pair that
with xtc_loop_wake() (or
xtc_proc_wake() for a parked proc) — relying
on raw fd readiness alone races the loop's park/re-arm window and can be
missed, leaving the loop asleep in
xtc_io_poll().
The nudge is lost-wake-free (it writes the loop's wakeup fd, which the
backends keep armed across the drain) and delivers no event, so a spurious
wake is always safe.
xtc_loop_fini()
frees every task still held by the loop, every timer ever created (live or
fired), the loop's xtc_io_t, and the loop struct
itself. Processes still alive on the loop (parked in a receive, a sleep, or
any other wait) are torn down with it: their queued messages, link and
monitor entries, and memory context are released, but their
xtc_proc_at_exit()
callbacks are not run and no “DOWN” or exit signal is sent,
because their fibers never resume. A process that must release an external
resource on shutdown should be told to exit, for example with
xtc_exit_pid()
(see xtc_proc(3)), and allowed to finish before the loop
is finalized. Messages sent to such a process after
xtc_loop_fini() are dropped.
TASKS
xtc_task_spawn() creates a new task in
XTC_TASK_RESCHED state and enqueues it. The function
pointer fn is invoked with the task handle and the
user pointer the caller supplied at spawn. The handle
written to *out_task is valid for the lifetime of the
loop.
WAKERS
xtc_task_waker()
fills a xtc_waker_t that the caller may store and
later use to re-queue the parked task via
xtc_waker_wake().
The waker names the loop the task is CURRENTLY runnable on at the moment
xtc_task_waker() is called -- for a work-stealable
task (xtc_proc(3)'s migratable),
this is correct even after the task has migrated to a different loop than
the one it was spawned on: the waker always reflects the present, not the
task's original loop. Arm it fresh (call
xtc_task_waker() again) before each park, from the
task itself, so a waker used across multiple park/resume cycles never goes
stale. A waker may be fired any number of times: only the first transition
from XTC_TASK_PENDING to
XTC_TASK_RESCHED re-queues the task; subsequent
wakes while the task is already scheduled or running are no-ops. A waker
fired after the task has reached XTC_TASK_DONE is a
safe no-op.
M3 wakers are valid only on the owning thread. Cross-thread wakeup arrives in M5.
PARKING
xtc_task_park_on_timer()
arranges for the task to be re-queued after at least
delay_ns nanoseconds.
xtc_task_park_on_fd()
registers the task with the loop's xtc_io_t for
readiness on fd. When the fd becomes ready, the loop
unregisters it and re-queues the task.
A task may have at most one outstanding park (timer or fd) at a
time. A second park while the first is in flight returns
XTC_E_INVAL. After parking, the task fn must return
XTC_TASK_PENDING.
TIMERS
xtc_timer_set()
schedules a callback to fire after at least delay_ns
nanoseconds. The handle written to *out remains valid,
including after the timer has fired (see
xtc_timer_cancel()), until it is passed to
xtc_timer_release() or the loop is finalised.
out may be NULL when no handle
is needed; the timer's memory is then reclaimed as soon as it fires.
xtc_timer_cancel()
prevents a not-yet-fired timer from firing. A cancel after the timer has
already fired is a safe no-op. A cancel on the loop's own thread removes the
timer from the heap at once; a cancel from another thread marks it, and the
loop drops it before the heap next grows, so cancelled timers never
accumulate.
xtc_timer_release()
gives up the handle. It does not cancel: a pending timer still fires unless
it was cancelled first. The timer's memory is reclaimed once it has fired or
been cancelled and its handle has been released. The handle must not be used
after the release. Every handle should be released or
cancelled-and-released: a handle that is never released keeps its timer
(about 128 bytes) allocated until xtc_loop_fini(),
which on a long-running loop is unbounded growth.
xtc_timer_release() was added in 1.55.1; before it,
every xtc_timer_set() timer, and every
xtc_future_with_timeout(),
was retained until xtc_loop_fini().
xtc_timer_cancel() and
xtc_timer_release() may be called from any
thread.
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
M3 is single-thread. A loop must be created, run, and finalised by the same OS thread. The cross-thread wakeup primitive baked into xtc_io_t exists but is not yet exposed at the loop / task level; that arrives with the multi-loop executor in M5.
EXAMPLES
static int hello(xtc_task_t *self, void *u) {
(void)self; (void)u;
printf("hello\n");
return XTC_TASK_DONE;
}
int main(void) {
xtc_loop_t *loop;
xtc_loop_init(&loop);
xtc_task_spawn(loop, hello, NULL, NULL);
xtc_loop_run(loop);
xtc_loop_fini(loop);
return 0;
}
SEE ALSO
HISTORY
First appeared in xtc 0.0.1 (M3).
| May 25, 2026 | Debian |