libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
loop_int.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/loop_int.h
6 * Internal definitions for the L2 event loop. Not part of the
7 * public ABI.
8 */
9
10#ifndef XTC_LOOP_INT_H
11#define XTC_LOOP_INT_H
12
13#include <stdatomic.h>
14#include <stdint.h>
15
16#include "xtc_loop.h"
17#include "xtc_io.h"
18#include "xtc_res.h"
19#include "deque.h"
20#include "os_thread.h"
21
22/*
23 * Task state machine. Transitions:
24 * SCHEDULED -> RUNNING loop pops from queue
25 * RUNNING -> SCHEDULED fn returned RESCHED
26 * RUNNING -> PARKED fn returned PENDING
27 * RUNNING -> DONE fn returned DONE
28 * PARKED -> SCHEDULED waker fired (or timer / fd ready)
29 * PARKED -> DONE (not reachable; PENDING tasks
30 * are reaped only after they
31 * next return DONE)
32 */
33enum xtc_task_state {
34 XTC_TS_SCHEDULED = 0,
35 XTC_TS_RUNNING = 1,
36 XTC_TS_PARKED = 2,
37 XTC_TS_DONE = 3
38};
39
40struct xtc_task {
41 xtc_task_fn fn;
42 void *user;
43 xtc_loop_t *loop;
44 int state;
45 /* Monotonic time (ns) this run quantum was dispatched, recorded
46 * by the scheduler when loop->yield_budget_ns > 0. xtc_yield_check
47 * compares against it; 0 means not yet recorded this quantum. */
48 int64_t run_start_ns;
49 /* Pinned tasks run only on their home loop -- they go on the
50 * owner-only FIFO, never the stealable deque. Used for explicit
51 * placement (xtc_exec_spawn_on) and for processes, which keep a
52 * shard-style affinity to one loop. Unpinned tasks (the general
53 * pool) go on the Chase-Lev deque and may be work-stolen. */
54 int pinned;
55 /* Run-queue intrusive next pointer. */
56 struct xtc_task *q_next;
57
58 /* Park bookkeeping. At most one of these is active at a time
59 * while the task is in PARKED state. */
60 xtc_timer_t *park_timer;
61 int park_fd; /* -1 when not parked on fd */
62 /* Voluntary park: when set by a primitive (e.g. xtc_amutex) just
63 * before yielding, the coro step returns PENDING instead of
64 * RESCHED, so the task sleeps until a waker re-enqueues it rather
65 * than busy-spinning. Read-and-cleared by the step. */
66 int park_requested;
67
68 /* Wakeup-cause flags, set by the dispatcher / timer callback /
69 * mbox_deliver when the task is unparked. Sampled and cleared
70 * by the parker on resume (e.g. xtc_proc_wait_fd). Encodes
71 * XTC_IO_* flags from the dispatched event plus the synthetic
72 * XTC_WAIT_MAILBOX (set by __mbox_deliver) and XTC_WAIT_TIMEOUT
73 * (set by the timer callback). Atomic: a cross-thread xtc_send
74 * ORs XTC_WAIT_MAILBOX in from a FOREIGN thread (proc.c) while the
75 * owning loop ORs/reads/zeros it (task.c/loop.c/proc.c), so a plain
76 * uint32_t RMW here is a data race (TSan-reportable). All accesses
77 * use relaxed atomics -- ordering is provided by the waker/mailbox
78 * lock; the atomic only makes the OR itself race-free. */
79 _Atomic uint32_t wake_revents;
80
81 /* Doubly linked into loop->all_tasks so a completed task can be
82 * unlinked in O(1) and recycled to the loop's task_slab (instead of
83 * lingering until loop_fini). all_prev == NULL means the head. */
84 struct xtc_task *all_next;
85 struct xtc_task *all_prev;
86
87 /* 1 if this task struct is eligible to be recycled onto the loop's
88 * task free-list when it completes (a plain task on its home loop);
89 * cleared for tasks that must not be recycled. All task structs
90 * are __os_calloc'd regardless -- this only gates the free-list
91 * push, not the allocation source. */
92 int recyclable;
93
94 /* Optional cleanup hook invoked by xtc_loop_fini before the task
95 * struct is freed. The coroutine layer sets this to release the
96 * fiber stack + coro struct that wrap a task; plain tasks leave
97 * it NULL. Keeps task lifetime owned by the loop while letting
98 * higher layers reclaim what they attached. */
99 void (*cleanup)(void *cleanup_arg);
100 void *cleanup_arg;
101};
102
103/*
104 * Timer record. Kept in a binary min-heap inside the loop.
105 *
106 * Cancel is lazy: we mark cancelled and skip on pop. Cancel is O(1)
107 * by cost; the heap may carry up to N stale entries until the next
108 * extraction reaches them. For M3 this is good enough; M5 may
109 * upgrade to a hierarchical wheel.
110 */
111struct xtc_timer {
112 int64_t deadline_ns;
113 xtc_timer_fn cb;
114 void *user;
115 xtc_task_t *waiter; /* task to wake when fired (NULL if pure cb) */
116 int heap_idx; /* current position in heap (-1 if not in) */
117 int cancelled;
118 int fired;
119 int sim_late; /* DST: 1 once a buggify late-fire bumped
120 * this timer's deadline (bump at most
121 * once so a late fire cannot spin);
122 * always 0 outside sim. */
123 xtc_loop_t *loop; /* back-pointer for cancel-by-handle */
124 struct xtc_timer *all_next; /* per-loop linked list for cleanup */
125};
126
127/*
128 * Inbox message kinds. All inbox traffic is cross-thread; the loop
129 * owner drains its inbox at the top of every step.
130 */
131enum xtc_inbox_kind {
132 XTC_INB_WAKE = 0, /* re-queue a parked task */
133 XTC_INB_PUBLISH = 1, /* publish a freshly-allocated task */
134};
135
137 enum xtc_inbox_kind kind;
138 xtc_task_t *task;
139 struct xtc_inbox_msg *next;
140};
141
142struct xtc_inbox {
143 __os_mutex_t lock;
144 struct xtc_inbox_msg *head;
145 struct xtc_inbox_msg *tail;
146 int inited;
147};
148
149struct xtc_loop {
150 xtc_io_t *io;
151
152 /* Local run queue (Chase-Lev deque, owner pushes/pops). */
153 xtc_deque_t deque;
154
155 /* Slow-path overflow when the deque is full. Owner-only. */
156 struct xtc_task *q_head;
157 struct xtc_task *q_tail;
158
159 /* Timer min-heap. */
160 xtc_timer_t **timers;
161 int n_timers;
162 int cap_timers;
163
164 /* All tasks ever spawned, for cleanup. Owner-only after init. */
165 struct xtc_task *all_tasks;
166
167 /* All timers ever created, for cleanup at fini. */
168 xtc_timer_t *all_timers;
169
170 /* M11.5b: per-loop slab cache for xtc_timer_t. Created lazily
171 * by xtc_timer_set; freed in loop_fini. Per-loop = single-
172 * threaded ownership = magazine fast path is lock-free. */
173 struct xtc_slab *timer_slab;
174
175 /* Per-loop task-struct free-list: a plain single-threaded LIFO of
176 * recycled task structs (linked through their q_next while free).
177 * xtc_task_spawn pops from it instead of malloc'ing, and a
178 * completed plain task on its home loop is pushed back instead of
179 * freed -- the spawn-heavy hot path, with no allocator call and no
180 * accumulation. Only the owning loop thread touches it, so it is
181 * lock-free. Drained (structs __os_free'd) at loop_fini. */
182 struct xtc_task *task_free;
183 int task_free_n;
184
185 /* Live-task counter. Atomic so cross-thread spawns/completions
186 * can update it without lock. */
187 _Atomic int n_alive;
188
189 /* Per-loop work statistics (executor observability). tasks_run
190 * counts task steps executed on this loop; steals counts tasks
191 * this loop successfully stole from a peer. Relaxed atomics:
192 * read-mostly counters, exactness across a concurrent read is
193 * not required. */
194 _Atomic uint64_t n_tasks_run;
195 _Atomic uint64_t n_steals;
196
197 /* I/O fairness: counts task runs since the last I/O poll. When the
198 * run queue never empties (busy-yielding fibers, e.g. a buffer
199 * manager spinning on eviction), the loop would otherwise never
200 * poll I/O and parked completions would starve. Every
201 * IO_FAIRNESS_QUANTUM runs the step does a non-blocking poll. */
202 unsigned int runs_since_poll;
203
204 /* Cooperative yield watchdog (opt-in). When yield_budget_ns > 0
205 * the scheduler records each quantum's start time on the task and
206 * xtc_yield_check reports a task over budget; n_yield_due counts
207 * over-budget reports (telemetry). */
208 int64_t yield_budget_ns;
209 _Atomic uint64_t n_yield_due;
210
211 int stop_requested;
212
213 /* Cross-thread inbox: wakers and remote spawns deposit here;
214 * the owner drains in __xtc_loop_drain_inbox. */
215 struct xtc_inbox inbox;
216
217 /* For the multi-loop executor: 0-based index in xtc_exec; -1 if
218 * this loop is standalone (M3 single-thread mode). */
219 int exec_id;
220
221 /* Back-pointer to the executor (NULL if standalone). */
222 struct xtc_exec *exec;
223
224 /*
225 * Resource accountant. Either owned by the loop (allocated and
226 * freed at init/fini) or borrowed from the executor. Tracks
227 * tasks-alive, inbox messages, channels, etc.
228 */
229 xtc_res_t *res;
230 int owns_res;
231};
232
233/* Internal helpers shared between loop.c, task.c, timer.c. */
234int __xtc_loop_enqueue(xtc_loop_t *loop, xtc_task_t *t);
235/* Spawn with explicit pinned-ness: pinned tasks stay on `loop` (FIFO,
236 * never work-stolen); unpinned tasks may migrate via the deque. */
237int __xtc_task_spawn_ex(xtc_loop_t *loop, xtc_task_fn fn, void *user,
238 int pinned, xtc_task_t **out_task);
239int __xtc_timer_heap_push(xtc_loop_t *loop, xtc_timer_t *t);
240xtc_timer_t *__xtc_timer_heap_pop_due(xtc_loop_t *loop, int64_t now_ns);
241int64_t __xtc_timer_heap_next_deadline(xtc_loop_t *loop);
242int __xtc_loop_dispatch_event(xtc_loop_t *loop, xtc_io_event_t *ev);
243
244/* Implemented in proc.c. Called from loop_fini to release the
245 * proc-table side struct hashed against this loop pointer. Must be
246 * idempotent. */
247void __xtc_proc_loop_unregister(xtc_loop_t *loop);
248
249/* Inbox API. Producer-side functions are thread-safe. */
250int __xtc_inbox_init(struct xtc_inbox *ib);
251void __xtc_inbox_fini(struct xtc_inbox *ib);
252int __xtc_inbox_push(struct xtc_inbox *ib, enum xtc_inbox_kind k, xtc_task_t *t);
253int __xtc_inbox_drain(xtc_loop_t *loop); /* owner-only; drains into local queue */
254
255/* Per-thread cursor: which loop the calling thread is running.
256 * NULL on threads that aren't loop owners. */
257extern XTC_THREAD_LOCAL xtc_loop_t *__xtc_current_loop;
258
259/*
260 * Fiber-context preservation hook. The L3 process layer keeps a
261 * per-thread "current proc" pointer that must survive a yield (the
262 * scheduler runs other fibers in between, which overwrite it). The
263 * L2 coro layer cannot depend on L3, so every yield/await jump saves
264 * the opaque context before jumping to the scheduler and restores it
265 * on resume through these hooks. proc.c installs them on first
266 * spawn; while NULL (no process layer in use) the calls are no-ops.
267 * Set once to stable function addresses, so a plain pointer load is
268 * safe without synchronization.
269 */
270extern void *(*__xtc_fiber_ctx_save)(void);
271extern void (*__xtc_fiber_ctx_restore)(void *);
272
273/* Post-resume cancellation hook. Installed by the process layer
274 * (proc.c). Called at the universal fiber resume point (after a yield
275 * returns) so a fiber that had a kill/cancel requested while it was
276 * NOT at a cooperative point -- e.g. a pure CPU loop that was
277 * involuntarily preempted -- honors the kill the instant the scheduler
278 * resumes it, by unwinding via xtc_exit_self. NULL when no process
279 * layer is present (bare coroutine use). Returns without effect if no
280 * kill is pending. */
281extern void (*__xtc_fiber_kill_check)(void);
282
283/* Forward declaration for back-pointer in xtc_loop. */
284struct xtc_exec;
285
286#endif /* XTC_LOOP_INT_H */