xtc_preempt(3)

---

xtc_preempt(3)

per-worker preemption timer seam

XTC_PREEMPT(3) Library Functions Manual XTC_PREEMPT(3)

xtc_preempt_arm, xtc_preempt_disarm, xtc_preempt_supported, xtc_preempt_ticks, xtc_preempt_tick_pendingper-worker preemption timer seam

#include <xtc_preempt.h>

int
xtc_preempt_arm(int64_t interval_ns);

int
xtc_preempt_disarm(void);

int
xtc_preempt_supported(void);

uint64_t
xtc_preempt_ticks(void);

int
xtc_preempt_tick_pending(void);

These functions manage a per-thread preemption timer -- the seam the libxtc preemption facility is built on (see the preemption design in the project docs). On most platforms (Linux, the BSDs, illumos, AIX) the timer measures the calling thread's own CPU time, so a busy fiber accrues ticks while an idle worker does not; on macOS the tick source is instead wall-clock (see the macOS paragraph below for why and the consequence).

() arms a timer on the calling thread that fires every interval_ns of the underlying tick source (that thread's CPU time on most platforms; wall-clock time on macOS), delivering an internal signal to the thread. Re-arming re-sets the interval. It returns XTC_OK, XTC_E_INVAL for a non-positive interval, or XTC_E_NOSYS where no tick source is available.

() stops and deletes the calling thread's timer; it is safe to call when no timer is armed.

() returns non-zero where the platform provides a tick source -- per-thread CPU-time timers on Linux, the BSDs, illumos, and AIX, or a kqueue EVFILT_TIMER on macOS (see below) -- and zero otherwise, in which case xtc_preempt_arm() returns XTC_E_NOSYS.

() returns the total number of timer ticks the calling thread has observed since arming -- telemetry, and the metric that confirms the seam works.

() returns non-zero if a tick has fired and is unconsumed, clearing the flag. The cooperative-assisted preemption path consults it at safe points to decide whether to yield.

The int functions return XTC_OK on success or a negative XTC_E_* code as described above.

The timer only records ticks; it does not by itself preempt a running fiber. Higher layers of the preemption facility act on the ticks -- cooperatively (a yield at the next safe point) or, when the involuntary path is enabled, via a signal-context involuntary yield. Nothing arms the timer unless a caller opts in, so the cooperative fast path is unchanged when unused.

The involuntary path (enabled by ()) is EXPERIMENTAL and off by default. On x86-64 with the ucontext coroutine substrate (the default build on glibc) it performs a true resumable involuntary yield -- a fiber in a pure CPU loop with no cooperative yield points is time-sliced so its loop-mates make progress -- using an on-stack trampoline that the signal handler redirects the interrupted instruction pointer to (Go's async-preemption method). On other substrates (the fcontext substrate used on musl, the Windows fiber substrate) and in the single-file amalgamation it declines and falls back to the cooperative path. It is opt-in because a rare livelock has been observed under pathological all-CPU-bound load; untrusted pure-CPU work can alternatively run on xtc_osproc(3), an operating-system thread the kernel preempts.

On macOS the involuntary path (Phase 2, the signal-context redirect) remains Linux-only: it needs the fiber's interrupted machine state rewritten via an on-stack trampoline (see SEE ALSO), and that redirect is gated inside the active coroutine substrate, which declines on __APPLE__. The Phase 1 cooperative-assisted tick source, however, DOES now work on macOS: since there is no per-thread () / CLOCK_THREAD_CPUTIME_ID (process-wide () is unsuitable for per-thread accounting), macOS instead drives xtc_preempt_arm() with a kqueue EVFILT_TIMER on a small private per-thread kqueue: a dedicated helper thread blocks in () and, on every timer fire, delivers the SAME internal signal a POSIX-timer platform's kernel would, driving the identical tick counter and pending flag this page documents above. The one real, user-visible difference: kqueue has no CPU-time filter, so the macOS tick source is WALL-CLOCK, not CPU-time -- an idle macOS worker still ticks, where an idle Linux/BSD worker does not. Net effect: on macOS xtc_preempt_arm correctly drives Phase 1 cooperative-assisted preemption (a fiber that calls a yield point periodically IS time-sliced), but still degrades to the cooperative path only for Phase 2 -- a fiber that never reaches a yield point will not be involuntarily interrupted on macOS. This Phase-2 gap is a real limitation, not merely an untested corner; do not rely on involuntary fairness for uncooperative CPU-bound work on macOS. The Phase 1 kqueue tick source has been reviewed and cross-compiled for macOS but not yet runtime-verified on real Apple hardware by the change that introduced it; see docs/KNOWN_ISSUES.md for the current verification status.

On the io_uring backend the executor drives the cooperative (Phase 1) tick source from the ring itself rather than from this per-thread signal timer: it arms a rearmed IORING_OP_TIMEOUT SQE on a small dedicated ring, and the “preempt is due” check becomes two relaxed/acquire loads of that ring's completion head/tail -- no signal, no syscall, and none of the wrong-thread signal-delivery hazard the CPU-time timer historically fought. A long compute fiber that reaches a yield point is therefore time-sliced with NO signal delivered on that backend (measured at roughly a third the per-check cost of the signal-flag path). The signal timer this page documents remains the portable Phase 1 source on epoll, kqueue, IOCP, select, and the other readiness backends, where there is no ring to read; and it is still armed on io_uring too whenever the involuntary (Phase 2) path is enabled, since redirecting a fiber that never reaches a yield point requires the signal handler. This ring-pointer preempt mechanism is INSPIRED BY Glommio (Glauber Costa / ScyllaDB): its () (glommio src/reactor.rs and src/sys/uring.rs preempt_pointers) is likewise two loads of a dedicated latency ring's head/tail.

xtc_async(3), xtc_exec(3), xtc_osproc(3)

The XTC Project.

July 1, 2026 Debian

View the mdoc source