xtc_preempt(3)
---xtc_preempt(3)
per-worker preemption timer seam
| XTC_PREEMPT(3) | Library Functions Manual | XTC_PREEMPT(3) |
NAME
xtc_preempt_arm,
xtc_preempt_disarm,
xtc_preempt_supported,
xtc_preempt_ticks,
xtc_preempt_tick_pending —
per-worker preemption timer seam
SYNOPSIS
#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);
DESCRIPTION
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).
xtc_preempt_arm()
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.
xtc_preempt_disarm()
stops and deletes the calling thread's timer; it is safe to call when no
timer is armed.
xtc_preempt_supported()
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.
xtc_preempt_ticks()
returns the total number of timer ticks the calling thread has observed
since arming -- telemetry, and the metric that confirms the seam works.
xtc_preempt_tick_pending()
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.
RETURN VALUES
The int functions return
XTC_OK on success or a negative
XTC_E_* code as described above.
NOTES
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
xtc_preempt_set_involuntary())
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
timer_create()
/ CLOCK_THREAD_CPUTIME_ID (process-wide
setitimer()
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
kevent()
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
need_preempt()
(glommio src/reactor.rs and
src/sys/uring.rs preempt_pointers) is likewise two
loads of a dedicated latency ring's head/tail.
SEE ALSO
AUTHORS
The XTC Project.
| July 1, 2026 | Debian |