Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
scheduler.c
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <[email protected]>
4 *
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#include <stdatomic.h>
20
21#include "serene/rt/context.h"
22#include "serene/rt/engine.h"
23#include "serene/rt/fiber.h"
26#include "serene/rt/reactor.h"
27#include "serene/utils.h"
28
29#define SCHED_LOG(FMT, ...) DBG("SCHED", FMT __VA_OPT__(, ) __VA_ARGS__)
30
31/// Per-operation deque and queue tracing (push, pop, steal, wake). This fires
32/// on the hot path, so it floods a debug build and is off unless
33/// SRN_SCHED_TRACE is defined. SCHED_LOG (scheduler lifecycle, stop, shutdown
34/// reap) stays on in a debug build. Both are silent in release, since DBG is.
35#ifdef SRN_SCHED_TRACE
36# define SCHED_TRACE(FMT, ...) DBG("SCHED", FMT __VA_OPT__(, ) __VA_ARGS__)
37#else
38# define SCHED_TRACE(...) \
39 do { \
40 } while (0)
41#endif
42
43// -----------------------------------------------------------------------------
44// Model
45// -----------------------------------------------------------------------------
46// The scheduler decides what runs next and owns the global queue and the
47// registry. It does not run fibers. The os threads do. Each os thread runs the
48// worker routine over its own worker, which holds a local run queue. The
49// routine finds a fiber -- the worker's local queue first, then the global
50// queue, then stealing one from a peer -- switches into it, and handles how it
51// gives up control. A fiber yields, suspends, or finishes by switching to its
52// worker's loop, which is therefore the resumer. The single scheduler is shared
53// by every worker.
54//
55// Migration hygiene. A fiber may run on any worker and resume on a different
56// one than it last ran on, so it carries no thread identity. The only
57// thread-bound state is the `current_worker` thread-local, read fresh at each
58// use and never stored on a fiber across a switch. Allocating through a fiber's
59// context is safe from any worker, since the memory manager locks its own
60// blocks, and a context is the shared engine arena rather than a per-thread
61// one. The one rule the IO layer must keep is to read `errno` before any
62// suspend or yield, since the fiber may resume on another thread where `errno`
63// differs.
64
65// -----------------------------------------------------------------------------
66// Work-stealing
67// -----------------------------------------------------------------------------
68// Each worker owns a Chase-Lev work-stealing deque (a fixed ring of ready
69// fibers, see srn_worker_t), and the scheduler keeps one shared global queue
70// besides. Two signed, monotonically increasing counters index a worker's ring.
71// `bottom` is the owner's end and `top` is the thieves' end. The live slot for
72// an index is `index & (SRN_FIBER_LOCAL_RING_CAP - 1)`.
73//
74// top (thieves take the oldest) bottom (owner pushes/pops the newest)
75// | |
76// [ f3 ][ f4 ][ f5 ][ f6 ][ f7 ]
77//
78// The owner pushes and pops `bottom`, and since only it touches that end the
79// common path is lock free and uncontended. Thieves take from `top` with a
80// compare-and-swap, so several can race and exactly one wins. The owner and the
81// thieves touch opposite ends, so they collide only over the last remaining
82// element, a race `local_pop` and `local_steal` settle with a seq-cst fence and
83// the CAS on `top`. The counters are signed so the transient `bottom - 1` on an
84// empty deque reads as -1 ("empty") rather than wrapping.
85//
86// Finding work. A worker drains its own deque, then the global queue, then
87// steals one fiber from each peer in turn (`find_work`). Work made while a
88// fiber runs (a yield, spawn, or wake) goes onto the running worker's own
89// deque, keeping it local. Work from off a worker, or work that overflows a
90// full deque, goes to the global queue. So the global lock stays off the hot
91// path while every os thread is busy -- it is taken only for the global queue
92// and to wake a parked os thread (when one exists).
93//
94// The deque follows the weak memory correct formulation, so the fences are
95// right on ARM and the like, not only on x86:
96// - Chase & Lev, "Dynamic Circular Work-Stealing Deque", SPAA 2005.
97// https://doi.org/10.1145/1073970.1073974
98// - Le, Pop, Cohen & Zappa Nardelli, "Correct and Efficient Work-Stealing for
99// Weak Memory Models", PPoPP 2013. https://doi.org/10.1145/2442516.2442524
100// (PDF: https://fzn.fr/readings/ppopp13.pdf)
101
102/// Defined here, not in fiber.h, which only forward declares `srn_scheduler_t`.
103/// Consumers hold a `srn_scheduler_t *` and never see the layout. Two reasons:
104///
105/// 1. The layout can evolve without recompiling or disturbing consumers. Going
106/// M:N this struct grows per-thread local queues, a worker array, a reactor
107/// handle, steal state -- none of which should ripple into every
108/// translation unit that includes `fiber.h`.
109///
110/// 2. It makes the decide/execute boundary physical. The scheduler owns the
111/// ready queue and the picking policy. The worker routine and fibers must
112/// reach it only through enqueue/yield/ready. Keeping the fields private
113/// means no
114/// caller *can* poke the queue directly -- the encapsulation is the
115/// contract, enforced by the compiler rather than by convention.
117
118/// The scheduler's lifecycle as one atomic value. `RUNNING` means a run is
119/// servicing the queues. `DRAINING` is the graceful wind down set by
120/// `srn_sched_drain`, workers keep running every runnable fiber and let
121/// in-flight IO finish, but new IO submissions are fenced (see
122/// `srn_sched_accepting_submissions`) so fibers unwind instead of parking on
123/// fresh ops, and the pool converges to quiescence. `STOPPING` tells the
124/// workers to wind down at once, set at natural quiescence or abruptly by
125/// `srn_sched_stop`. `IDLE` is the resting state before a run starts. Workers
126/// read it without the lock, so it is atomic.
127///
128/// The order matters, a state at or past `DRAINING` no longer accepts new IO,
129/// which `srn_sched_accepting_submissions` relies on.
136
139 /// Global lock. Guards the global/overflow queue, the registry, and the
140 /// worker coordination fields below. It does NOT guard the per-worker local
141 /// queues, which carry their own locks. Lock order is global-before-local, a
142 /// worker may hold this while taking a local lock (only the park scan does),
143 /// but a local lock is never held while taking this.
145 /// Global / overflow queue. Holds fibers enqueued with no current worker --
146 /// an external waker, or the initial fibers made before the run. A worker
147 /// drains its own local queue first, then this. FIFO through the intrusive
148 /// `link`.
151 /// Registry, head of the doubly-linked list (through `reg_prev`/`reg_next`)
152 /// of every live fiber, on a different axis from the run queues. A fiber
153 /// joins at srn_fiber_make and leaves when reaped, so the list is every fiber
154 /// the scheduler is still responsible for, including SUSPENDED ones that sit
155 /// on no run queue. It is how the scheduler accounts for and cleans them up.
157
158 /// Worker coordination. Parked os threads wait on `work`. `idle` counts
159 /// parked os threads and `runnable` counts fibers waiting in ANY queue (local
160 /// or global). Both are atomic because a push reads `idle`, and the park path
161 /// reads `runnable`, without holding the lock the other side updates them
162 /// under. The "idle++ then read runnable" park ordering against the
163 /// "runnable++ then read idle" push ordering is what makes a lost wakeup
164 /// impossible.
165 ///
166 /// WARNING: that pairing is correct ONLY because all four of those operations
167 /// are seq_cst (the default for `atomic_fetch_add` and `atomic_load`).
168 /// `announce_work` does its `runnable++` and its `idle` read with NO lock
169 /// held, so the single seq_cst total order is the only thing tying it to the
170 /// park path. Do NOT weaken these to acq_rel or relaxed for "speed". Weaken
171 /// them and a push and a park can each fail to see the other, so an os thread
172 /// sleeps on `work` forever while a runnable fiber sits in a queue. That is a
173 /// lost wakeup, a hang. If these ever must be relaxed, the whole
174 /// `runnable`/`idle` handshake has to move under the lock first, the way
175 /// `global_enqueue` already does it, so the lock supplies the ordering the
176 /// weaker atomics would not.
177 ///
178 /// `nworkers` is fixed for a run. `state` drives termination. An os thread
179 /// stops once it observes `SRN_SCHED_STOPPING`, set at quiescence (idle ==
180 /// nworkers and runnable == 0) or by `srn_sched_stop`.
182 atomic_size_t idle;
183 atomic_size_t runnable;
184 size_t nworkers;
185
187 /// `srn_sched_run` allocates these two arrays and `srn_sched_shutdown` frees
188 /// them. They live until shutdown, so shutdown can join the threads and reap.
189 /// The scheduler struct itself is immortal, but these arrays are not.
190 ///
191 /// `workers` holds all `nworkers` workers in one array, so each worker can
192 /// find the others to steal from. `os_threads` holds the OS threads the
193 /// scheduler started. There is not one thread per worker. The caller's own
194 /// thread runs worker 0, so the scheduler never starts a thread for it. Only
195 /// workers 1 through nworkers-1 get a thread, so slot 0 of `os_threads` is
196 /// unused and shutdown joins slots 1 through nworkers-1. A thread belongs
197 /// here, not in `srn_worker_t`, because it marks a thread the scheduler
198 /// started and must join, which is not the same as being a worker.
201 /// True for the duration of an `srn_sched_run` call. `srn_sched_shutdown`
202 /// reads it to reject being called while a run is still in flight (it must
203 /// run after `run` has returned). Atomic because shutdown may read it from a
204 /// different thread than the one inside `run`.
205 _Atomic bool run_active;
206 /// Set once `srn_sched_shutdown` has torn the scheduler down. The scheduler
207 /// is not usable afterwards, a further `run` panics, and a further `shutdown`
208 /// is a no-op.
210};
211
212/// Capacity of each worker's local work-stealing deque. Must be a power of two:
213/// the live slot for a deque index is `index & (cap - 1)`. 256 matches the
214/// common choice (Go, Tokio). A fiber that does not fit overflows to the global
215/// queue. This is the single source of truth for the size, so a configuration
216/// layer can later drive it.
217#define SRN_FIBER_LOCAL_RING_CAP 256
218static_assert(
220 "SRN_FIBER_LOCAL_RING_CAP must be a power of two"
221);
222
223/// The state one os thread uses to run fibers. The worker's loop (`loop`)
224/// represents the os thread itself and is the resumer for every fiber this
225/// worker runs. Each worker owns a lock free Chase-Lev work stealing deque. The
226/// owner pushes and pops the `bottom` end, while thieves take from the `top`
227/// end. So the common push and pop touch no lock, and only a steal contends.
233
234 /// Chase-Lev deque. `top` and `bottom` are signed and monotonically
235 /// increasing (signed so the transient `bottom - 1` on an empty deque does
236 /// not wrap), and the live slot for an index is `index &
237 /// (SRN_FIBER_LOCAL_RING_CAP - 1)`.
238 /// TODO(lxsameer): Make the ring capacity configurable via CLI args.
239 atomic_intptr_t top;
240 atomic_intptr_t bottom;
241 _Atomic(srn_fiber_t *) ring[SRN_FIBER_LOCAL_RING_CAP];
242 // TODO(lxsameer): a per-thread ring of free stacks to recycle could live
243 // here.
244};
245
246/// The worker the calling os thread is running, or null when this os thread is
247/// not running the worker routine (srn_sched_run is not active on it). This
248/// thread-local is the seam that resolves the resumer, the current fiber, and
249/// "are we in a fiber?" -- all per os thread state that cannot live in the
250/// single shared scheduler.
251static _Thread_local srn_worker_t *current_worker = nullptr;
252
253// -----------------------------------------------------------------------------
254// Lifecycle
255// -----------------------------------------------------------------------------
256
258 PANIC_IF_NULL(engine);
259 // The scheduler outlives every context and fiber, so it is allocated from the
260 // immortal region rather than a releasable block.
262 PANIC_IF_NULL(sched);
263
264 sched->engine = engine;
265 sched->ready_head = nullptr;
266 sched->ready_tail = nullptr;
267 sched->registry = nullptr;
268 sched->idle = 0;
269 sched->runnable = 0;
270 sched->nworkers = 0;
271 sched->state = SRN_SCHED_IDLE;
272 sched->workers = nullptr;
273 sched->os_threads = nullptr;
274 sched->destroyed = false;
275 atomic_init(&sched->run_active, false);
276
277 PANIC_IF(
278 srn_mutex_init(&sched->lock) != SRN_THREAD_OK, "failed to initialise the scheduler lock"
279 );
280
281 PANIC_IF(
282 srn_cond_init(&sched->work) != SRN_THREAD_OK, "failed to initialise the scheduler condition"
283 );
284
285 // srn_engine_make will store this scheduler in the engine
286 return sched;
287}
288
289/// Insert at the head of the registry. Caller must hold `sched->lock`.
290static void registry_add(srn_scheduler_t *sched, srn_fiber_t *fiber) {
291 fiber->reg_prev = nullptr;
292 fiber->reg_next = sched->registry;
293
294 if (sched->registry != nullptr) {
295 sched->registry->reg_prev = fiber;
296 }
297
298 sched->registry = fiber;
299}
300
301/// Unlink from the registry. O(1), thanks to the back pointer. Caller must hold
302/// `sched->lock`.
303static void registry_remove(srn_scheduler_t *sched, srn_fiber_t *fiber) {
304 if (fiber->reg_prev != nullptr) {
305 fiber->reg_prev->reg_next = fiber->reg_next;
306 } else {
307 sched->registry = fiber->reg_next;
308 }
309
310 if (fiber->reg_next != nullptr) {
311 fiber->reg_next->reg_prev = fiber->reg_prev;
312 }
313
314 fiber->reg_prev = nullptr;
315 fiber->reg_next = nullptr;
316}
317
319 PANIC_IF_NULL(sched);
320 PANIC_IF_NULL(fiber);
321
322 srn_mutex_lock(&sched->lock);
323 registry_add(sched, fiber);
324 srn_mutex_unlock(&sched->lock);
325}
326
328 PANIC_IF_NULL(sched);
329
330 if (sched->destroyed) {
331 return;
332 }
333
334 // It must run on a thread outside the pool, a worker, or a fiber (which runs
335 // on a worker), would be tearing down the scheduler it is itself running on.
336 // `current_worker` is null only off a worker, so it is the test for that.
337 PANIC_IF(
338 current_worker != nullptr, "srn_sched_shutdown must be called from outside the worker pool"
339 );
340
341 // And it must run after `srn_sched_run` has returned, not while a run is in
342 // flight. Stop a running pool with `srn_sched_stop` and let `srn_sched_run`
343 // return first.
344 PANIC_IF(
345 atomic_load(&sched->run_active),
346 "srn_sched_shutdown called while srn_sched_run is active; call "
347 "srn_sched_stop and let the run return first"
348 );
349
350 // The run has returned, so worker 0 has stopped. The spawned os threads may
351 // still be winding down (`srn_sched_run` does not join them), so join them
352 // now. `os_threads` slot 0 is the inline worker 0, never spawned, so the
353 // spawned os threads to join are 1..nworkers-1.
354 for (size_t i = 1; i < sched->nworkers; i++) {
355 (void)srn_thread_join(&sched->os_threads[i]);
356 }
357
358 // Every worker is gone, so this runs single threaded now.
359 //
360 // Any fiber still in the registry never finished, it was left parked in
361 // SRN_FIBER_SUSPENDED with no party able to wake it (a deadlock), or was left
362 // queued when the run stopped early. Release its stack so it does not leak.
363 // The fiber structs themselves live in context blocks and are reclaimed with
364 // those blocks, not here. The scheduler is immortal-allocated, so it is not
365 // freed either.
366 //
367 // Unlike the reap path, this unmaps for good, shutdown runs after the workers
368 // are gone, so the per-thread stack ring from the fiber.h TODO no longer
369 // exists and there is nothing to recycle into. (Draining that ring, when it
370 // exists, also belongs here.)
371 srn_fiber_t *fiber = sched->registry;
372 while (fiber != nullptr) {
373 srn_fiber_t *next = fiber->reg_next;
374 SCHED_LOG(
375 "shutdown reaping unfinished fiber '%s' (never scheduled, or suspended with no waker?)",
376 fiber->name
377 );
378 // TODO(lxsameer): Free up the ring here as well
380 srn_fiber_on_reap(fiber);
381 fiber->reg_prev = nullptr;
382 fiber->reg_next = nullptr;
383 fiber = next;
384 }
385 sched->registry = nullptr;
386
387 // Release the run-scoped storage (srn_mm_free tolerates null, so a scheduler
388 // that never ran is fine).
389 srn_mm_free(sched->engine->mm, sched->os_threads);
390 srn_mm_free(sched->engine->mm, sched->workers);
391 sched->os_threads = nullptr;
392 sched->workers = nullptr;
393 sched->nworkers = 0;
394
395 // Destroy the synchronisation primitives. The scheduler is not usable after
396 // this, so they are not re-initialised. No worker holds or waits on them now,
397 // the join above made sure of that.
398 (void)srn_cond_destroy(&sched->work);
399 (void)srn_mutex_destroy(&sched->lock);
400
401 sched->destroyed = true;
402}
403
404/// Wake the os thread of one parked worker after a fiber has joined a queue.
405/// "Parked" means that os thread is asleep in `srn_cond_wait` because it found
406/// no runnable fiber anywhere. This is not a fiber suspending. It is the whole
407/// os thread blocked, and the notify wakes it so it looks again.
408///
409/// `runnable` is bumped first, then `idle` is read. Paired against the park
410/// path, which bumps `idle` then reads `runnable`, this ordering means the two
411/// sides can never both miss, so a wakeup is never lost. The notify takes the
412/// global lock (the condition's lock) but only when an os thread is actually
413/// parked, so the common busy case never touches it.
414///
415/// WARNING: this runs with NO lock around the `runnable++` and the `idle` read,
416/// so its only tie to the park path is the seq_cst total order. Both must stay
417/// seq_cst. RELAX EITHER AND THE WAKEUP CAN BE LOST (an os thread parked with a
418/// runnable fiber queued). See the `srn_scheduler_t` coordination comment for
419/// the full reasoning.
420static void announce_work(srn_scheduler_t *sched) {
421 PANIC_IF_NULL(sched);
422
423 atomic_fetch_add(&sched->runnable, 1);
424
425 if (atomic_load(&sched->idle) > 0) {
426 // We have idle os threads. Wake them up
427 srn_mutex_lock(&sched->lock);
428 SCHED_TRACE("waking a parked os thread (runnable=%ld)", (long)atomic_load(&sched->runnable));
429 srn_cond_notify_one(&sched->work);
430 srn_mutex_unlock(&sched->lock);
431 }
432}
433
434// The local deque is a Chase-Lev work-stealing deque. The fence placement
435// follows Le, Pop, Cohen and Zappa Nardelli, "Correct and Efficient
436// Work-Stealing for Weak Memory Models" (PPoPP 2013), so it is correct on
437// weakly-ordered CPUs, not just on x86's strong model. The buffer is fixed, so
438// there is no resize and no buffer reclamation. The `runnable` count is
439// adjusted by the enqueue path (push) and by find_work (the only taker).
440
441/// This operation is only for the owner of the ring. Push a fiber on the
442/// bottom. Returns false when the deque is full, so the caller can overflow it
443/// to the global queue. The caller has set `state`.
444static bool local_push(srn_worker_t *w, srn_fiber_t *fiber) {
445 PANIC_IF_NULL(w);
446 PANIC_IF_NULL(fiber);
447
448 intptr_t b = atomic_load_explicit(&w->bottom, memory_order_relaxed);
449 intptr_t t = atomic_load_explicit(&w->top, memory_order_acquire);
450 if (b - t >= (intptr_t)SRN_FIBER_LOCAL_RING_CAP) {
451 return false; // full
452 }
453
454 atomic_store_explicit(&w->ring[b & (SRN_FIBER_LOCAL_RING_CAP - 1)], fiber, memory_order_relaxed);
455 // After ^^^, the slot isn't published to thieves yet, because they decide
456 // what's live by reading bottom, which we haven't bumped
457
458 // Publish the slot. write before the bottom store that exposes it to a thief.
459 // This is the key barrier. It orders the slot write before the bottom bump
460 // that follows. Paired with a thief's `acquire-load` of bottom in
461 // `local_steal`, it guarantees, if a thief sees the new bottom, it also sees
462 // the fiber we just wrote, never a stale/garbage slot.
463 atomic_thread_fence(memory_order_release);
464 atomic_store_explicit(&w->bottom, b + 1, memory_order_relaxed);
465
466 SCHED_TRACE("worker %zu local-push fiber %p", w->id, (void *)fiber);
467 return true;
468}
469
470/// Owner only. Pop a fiber from the bottom, or null when empty. The seq_cst
471/// fence and the compare-and-swap settle the race with a thief over the last
472/// element.
474 PANIC_IF_NULL(w);
475
476 // Since bottom is local to the owner, there is no other writer competing to
477 // write to it. So a load/store is enough here no need for `atomic_fetch_sub`.
478 intptr_t b = atomic_load_explicit(&w->bottom, memory_order_relaxed) - 1;
479 atomic_store_explicit(&w->bottom, b, memory_order_relaxed);
480
481 atomic_thread_fence(memory_order_seq_cst);
482 intptr_t t = atomic_load_explicit(&w->top, memory_order_relaxed);
483
484 srn_fiber_t *fiber = nullptr;
485 if (t <= b) {
486 // Non-empty.
487 fiber =
488 atomic_load_explicit(&w->ring[b & (SRN_FIBER_LOCAL_RING_CAP - 1)], memory_order_relaxed);
489 if (t == b) {
490 // Last element. The owner and a thief can race for it, so settle it with
491 // the CAS on `top`. Exactly one of them wins.
492 if (
493 atomic_compare_exchange_strong_explicit(
494 &w->top, &t, t + 1, memory_order_seq_cst, memory_order_relaxed
495 )
496 ) {
497 SCHED_TRACE("worker %zu popped the last fiber %p", w->id, (void *)fiber);
498
499 } else {
500 SCHED_TRACE("worker %zu lost the last fiber %p to a thief", w->id, (void *)fiber);
501 fiber = nullptr; // the thief won
502 }
503 atomic_store_explicit(&w->bottom, b + 1, memory_order_relaxed);
504 }
505 } else {
506 // Empty. Restore bottom.
507 atomic_store_explicit(&w->bottom, b + 1, memory_order_relaxed);
508 }
509 return fiber;
510}
511
512/// Thief side. Take a fiber from `victim`'s top, or null when the deque is
513/// empty or a concurrent take won the race -- the caller then just moves to the
514/// next victim.
516 PANIC_IF_NULL(victim);
517
518 intptr_t t = atomic_load_explicit(&victim->top, memory_order_acquire);
519 // Pairs with the `seq_cst` fence in `local_pop`. The two fences force a
520 // single total order in which the owner (lowering bottom, fence, reading top)
521 // and this thief (reading top, fence, reading bottom) cannot both decide they
522 // got the last element.
523 // Basically this fence handles the steal race against local_pop.
524 // Note: Don't mixup `memory_order_seq_cst` with `memory_order_acquire` that
525 // we use for loading victim's bottom the next line.
526 atomic_thread_fence(memory_order_seq_cst);
527 // This acquire on bottom handles slot visibility against `local_push`
528 intptr_t b = atomic_load_explicit(&victim->bottom, memory_order_acquire);
529
530 srn_fiber_t *fiber = nullptr;
531 if (t < b) {
532 fiber =
533 atomic_load_explicit(&victim->ring[t & (SRN_FIBER_LOCAL_RING_CAP - 1)], memory_order_relaxed);
534 if (!atomic_compare_exchange_strong_explicit(
535 &victim->top, &t, t + 1, memory_order_seq_cst, memory_order_relaxed
536 )) {
537 fiber = nullptr; // lost the race
538 }
539 }
540 return fiber;
541}
542
543/// Append a fiber to the global/overflow queue. The caller has set its
544/// `state`. The push, the `runnable` bump, and the wake all run under the
545/// global lock, so this path is trivially serialized against the park path and
546/// needs no separate ordering argument.
547///
548/// Put a fiber on the global queue and wake a parked os thread if any. Unlike
549/// `announce_work`, the `runnable++`, the `idle` read, and the notify all
550/// happen under the lock, so this path is safe by mutual exclusion and does not
551/// lean on the seq_cst ordering the lockless path does.
552static void global_enqueue(srn_scheduler_t *sched, srn_fiber_t *fiber) {
553 srn_mutex_lock(&sched->lock);
554
555 fiber->link = nullptr;
556
557 if (sched->ready_tail == nullptr) {
558 sched->ready_head = fiber;
559 } else {
560 sched->ready_tail->link = fiber;
561 }
562 sched->ready_tail = fiber;
563
564 atomic_fetch_add(&sched->runnable, 1);
565
567 "global-push fiber %p (runnable=%ld)", (void *)fiber, (long)atomic_load(&sched->runnable)
568 );
569
570 if (atomic_load(&sched->idle) > 0) {
571 srn_cond_notify_one(&sched->work);
572 }
573 srn_mutex_unlock(&sched->lock);
574}
575
576/// Pop the head of the global queue, or null when empty. The `runnable`
577/// adjustment is left to `find_work`, the only taker.
579 srn_mutex_lock(&sched->lock);
580 srn_fiber_t *fiber = sched->ready_head;
581
582 if (fiber != nullptr) {
583 sched->ready_head = fiber->link;
584 if (sched->ready_head == nullptr) {
585 sched->ready_tail = nullptr;
586 }
587 fiber->link = nullptr;
588 }
589
590 srn_mutex_unlock(&sched->lock);
591 return fiber;
592}
593
594/// Put a runnable fiber on a queue, with its `state` already set to `READY`. A
595/// fiber enqueued while running on a worker goes onto that worker's local
596/// deque, keeping its work local. One enqueued from off a worker (the initial
597/// fibers made before the run, or an external waker), or one that does not fit
598/// a full local deque, goes to the global queue.
599static void push_ready(srn_scheduler_t *sched, srn_fiber_t *fiber) {
601
602 // The publish (local_push) happens before announce_work bumps `runnable`,
603 // so a thief that takes the fiber first transiently drives `runnable` to
604 // SIZE_MAX. Every check of the counter is `== 0`, which the wrap cannot
605 // satisfy, so this is benign; do not add `> 0` style or signed comparisons
606 // on `runnable` without fixing the ordering here.
607
608 // On a worker, try its own deque first, falling through on a full deque.
609 if (w != nullptr) {
610 if (local_push(w, fiber)) {
611 announce_work(sched);
612 return;
613 }
614
615 SCHED_TRACE("worker %zu local deque full, overflow fiber %p to global", w->id, (void *)fiber);
616 }
617
618 // Off a worker, or the deque was full, the global queue takes it.
619 global_enqueue(sched, fiber);
620}
621
623 PANIC_IF_NULL(sched);
624 PANIC_IF_NULL(fiber);
625
626 fiber->state = SRN_FIBER_READY;
627 push_ready(sched, fiber);
628}
629
631 PANIC_IF_NULL(fiber);
632
633 // The NEW to READY flip admits exactly one scheduler of this fiber, the
634 // same guard ready_fiber uses for SUSPENDED, so a double schedule panics
635 // at the losing call site instead of double enqueuing.
637 PANIC_IF(
638 !atomic_compare_exchange_strong(&fiber->state, &expected, SRN_FIBER_READY),
639 "srn_fiber_schedule needs a NEW fiber. A fiber is scheduled exactly once, "
640 "and a suspended one is woken with srn_fiber_ready"
641 );
643}
644
645/// Wake a parked fiber by flipping `SUSPENDED` to `READY` and enqueuing it.
646/// Only the flip's winner enqueues, so racing wakers cannot double-enqueue it,
647/// and a fiber that is not parked is left untouched. The scheduler does not
648/// check the awaited condition. A fiber woken early resumes, re-checks, and
649/// parks again.
650static void ready_fiber(srn_scheduler_t *sched, srn_fiber_t *fiber) {
652 if (atomic_compare_exchange_strong(&fiber->state, &expected, SRN_FIBER_READY)) {
653 push_ready(sched, fiber);
654 }
655}
656
657/// Find a fiber to run, the worker's own deque first, then the global queue,
658/// then a steal of one fiber from each peer in turn. Null when nothing is
659/// runnable anywhere this worker can reach. Decrements `runnable` for whatever
660/// it takes.
662 srn_scheduler_t *sched = w->sched;
663
664 srn_fiber_t *fiber = local_pop(w);
665 if (fiber == nullptr) {
666 fiber = global_take(sched);
667 }
668
669 if (fiber == nullptr) {
670 for (size_t i = 1; i < sched->nworkers; i++) {
671 // We start form the right side neighbour and with `i` growing we will
672 // eventually loop back to the left side neighbour in the workers array.
673 size_t index = (w->id + i) % sched->nworkers;
674 srn_worker_t *victim = &sched->workers[index];
675 fiber = local_steal(victim);
676
677 if (fiber != nullptr) {
678 SCHED_TRACE("worker %zu stole fiber %p from worker %zu", w->id, (void *)fiber, victim->id);
679 break;
680 }
681 }
682 }
683
684 if (fiber != nullptr) {
685 atomic_fetch_sub(&sched->runnable, 1);
686 }
687
688 return fiber;
689}
690
691/// Run the worker routine over `worker` on the calling os thread. Find a fiber,
692/// run it, handle how it gave up control, and park when nothing is runnable,
693/// until the pool is quiescent. Owns the `current_worker` thread-local for its
694/// duration. The hot path (`find_work` hitting the local deque, run) touches
695/// only this worker's own lock free deque. The global lock is reached to park,
696/// for the global queue, and on every yield, a yielded fiber goes to the
697/// global tail so its peers get a turn, which is the point of yielding.
699 srn_scheduler_t *sched = worker->sched;
701
702 for (;;) {
703 // We check for termination here, between fibers, so an os thread stops at a
704 // clean boundary even while its worker's deque still holds work. Whatever
705 // is left unrun is reclaimed by `srn_sched_shutdown` through the registry.
706 if (atomic_load(&sched->state) == SRN_SCHED_STOPPING) {
707 break;
708 }
709
710 // Drain this worker's reactor completions before looking for fibers. Runs
711 // on the worker, so srn_fiber_ready pushes the woken fibers onto its own
712 // deque, keeping them local.
714
715 srn_fiber_t *fiber = find_work(worker);
716 if (fiber == nullptr) {
717 // Nothing runnable here or in any peer to steal from, so park this os
718 // thread. Parking while every other os thread is already parked and
719 // nothing is queued means the pool is quiescent. Nothing running can
720 // produce more work, so the run ends. Move to STOPPING and wake every os
721 // thread to exit. The `runnable` re-check in the loop closes the race
722 // with a fiber enqueued between find_work and taking the lock, and
723 // absorbs spurious wakeups.
724 srn_mutex_lock(&sched->lock);
725 atomic_fetch_add(&sched->idle, 1);
726
727 if (
728 atomic_load(&sched->idle) == sched->nworkers && atomic_load(&sched->runnable) == 0 &&
729 (int)srn_reactor_idle(sched->engine->reactor)
730 ) {
731
732 // All the os threads are idle. Time to stop
733 atomic_store(&sched->state, SRN_SCHED_STOPPING);
734 srn_cond_notify_all(&sched->work);
735 }
736
737 while (atomic_load(&sched->runnable) == 0 &&
738 atomic_load(&sched->state) != SRN_SCHED_STOPPING &&
740 // No runnable fiber around and we are not stopping. A draining pool
741 // parks here too, waiting for its in-flight ops to complete and ready
742 // their fibers, so they can unwind. Only STOPPING ends the park.
743 srn_cond_wait(&sched->work, &sched->lock);
744 }
745
746 // it has woken. This os thread is no longer parked. Snapshot whether
747 // we're stopping, then drop the lock.
748 atomic_fetch_sub(&sched->idle, 1);
749 bool stop = atomic_load(&sched->state) == SRN_SCHED_STOPPING;
750 srn_mutex_unlock(&sched->lock);
751
752 if (stop) {
753 break;
754 }
755
756 continue;
757 }
758
759 worker->current = fiber;
760 // The worker routine is the single owner of the RUNNING transition, for a
761 // fiber's first run and every resume after a yield.
762 fiber->state = SRN_FIBER_RUNNING;
763 srn_fiber_switch(&worker->loop, fiber);
764
765 // `fiber` has switched back, and is not on any queue. How it gave up the
766 // CPU is read from the fiber. A parked fiber left a commit on itself
767 // (`park_commit` set), a finished one is `DONE`, and a yielded one is still
768 // `RUNNING`.
769 if (fiber->park_commit != nullptr) {
770 // Parked. It is fully off the CPU now, with its context saved, so this is
771 // the first moment it is safe to wake (by others). Stamp `SUSPENDED`
772 // here, not in `srn_fiber_suspend` before the switch, so the label only
773 // ever marks a fiber that is parked and safe to resume. A waker flipping
774 // `SUSPENDED` to `READY` can therefore never catch a fiber still parking.
775 fiber->state = SRN_FIBER_SUSPENDED;
776
777 // `park_commit`/`park_arg` are one-shot, carrying the commit across the
778 // switch. Clearing them now loses nothing, since the next suspend sets
779 // them again and the fiber never reads them on resume.
780 srn_fiber_park_fn commit = fiber->park_commit;
781 void *park_arg = fiber->park_arg;
782 fiber->park_commit = nullptr;
783 fiber->park_arg = nullptr;
784
785 // Run the commit now that the fiber is parked. It hands the fiber to its
786 // waker (a waiter list, the reactor, and so on), which reschedules it
787 // later. A true return means stay parked. A false return means the
788 // condition already held, so wake it back up.
789 if (!commit(fiber, park_arg)) {
790 ready_fiber(sched, fiber);
791 }
792 } else if (fiber->state == SRN_FIBER_DONE) {
793 // Detach the waiter list (fibers blocked in `srn_fiber_wait_for`) and
794 // drop the fiber from the registry under the global lock, which also
795 // guards the waiter list against `wait_for_park`. Then wake the waiters
796 // and free the stack outside the lock. Each waiter reads this fiber's
797 // result, which outlives the reap since only the stack is freed, not the
798 // struct.
799 srn_mutex_lock(&sched->lock);
800 srn_fiber_t *waiters = fiber->waiters;
801 fiber->waiters = nullptr;
802 registry_remove(sched, fiber);
803 srn_mutex_unlock(&sched->lock);
804
805 while (waiters != nullptr) {
806 srn_fiber_t *waiter = waiters;
807 // Advance before the wake reuses `link`
808 waiters = waiter->link;
809 ready_fiber(sched, waiter);
810 }
811
812 // TODO(lxsameer): Instead of freeing the stack, return it to the ring
813 // pool
815 srn_fiber_on_reap(fiber);
816 } else {
817 // Yielded. It is fully off the CPU now, with its context saved, so this
818 // is the first moment it is safe to put back on a queue, where another
819 // worker may take it at once. `srn_fiber_yield` does not enqueue before
820 // switching, which would expose a context still being saved to a resuming
821 // worker. The fiber goes to the global queue tail, not the local deque:
822 // the worker pops its deque LIFO, so a local push would run the same
823 // fiber again immediately and starve its peers, making yield a no-op.
824 fiber->state = SRN_FIBER_READY;
825 global_enqueue(sched, fiber);
826 }
827
828 worker->current = nullptr;
829 }
830
831 current_worker = nullptr;
832}
833
834/// The entry an os thread starts in. It sets up its worker's loop -- on its own
835/// os thread, so the sanitizer captures the right stack bounds -- then runs the
836/// worker routine until the pool is quiescent. `arg` is the worker.
837static void worker_main(void *arg) {
838 srn_worker_t *worker = arg;
841}
842
843void srn_sched_run(srn_scheduler_t *sched, size_t nworkers) {
844 PANIC_IF_NULL(sched);
845 PANIC_IF(sched->destroyed, "srn_sched_run called on a scheduler that was already shut down");
846
847 // Claiming run_active up front turns an overlapping run into a clean panic
848 // instead of two runs clobbering the worker arrays under each other.
849 PANIC_IF(
850 atomic_exchange(&sched->run_active, true),
851 "srn_sched_run called while another run is active on this scheduler"
852 );
853
854 // A finished run leaves its worker arrays behind for shutdown to join and
855 // free. A second run would replace them while stragglers from the first may
856 // still be winding down, reviving those stragglers against the new run's
857 // state, and the reactor cannot be activated twice either. A scheduler
858 // therefore runs once; re-run support requires reactor reactivation.
859 PANIC_IF(
860 sched->workers != nullptr,
861 "srn_sched_run called on a scheduler that has already run; re-run is not "
862 "supported (the reactor cannot be reactivated)"
863 );
864
865 // A caller that does not pick a count gets the configured one, and every
866 // request is clamped to the configured ceiling. SRN_MAX_WORKERS stays the
867 // absolute ceiling above whatever the configuration asks for.
868 const srn_configuration_t *config = &sched->engine->config;
869 if (nworkers < 1) {
870 nworkers = config->fiber.workers;
871 }
872 if (nworkers > config->fiber.max_workers) {
873 nworkers = config->fiber.max_workers;
874 }
875 if (nworkers > SRN_MAX_WORKERS) {
876 nworkers = SRN_MAX_WORKERS;
877 }
878 if (nworkers < 1) {
879 nworkers = 1;
880 }
881
882 // Allocate `workers` and `os_threads` on the scheduler so
883 // `srn_sched_shutdown` can join the threads and free them later. `workers`
884 // has one entry per worker. `os_threads` has one per spawned thread, with
885 // slot 0 left empty because the caller runs worker 0 inline (see the struct
886 // comment). `runnable` is left alone, it already counts the fibers queued
887 // before the run.
888 sched->workers = srn_mm_malloc(sched->engine->mm, nworkers * sizeof(srn_worker_t));
889 PANIC_IF_NULL(sched->workers);
890
891 sched->os_threads = srn_mm_malloc(sched->engine->mm, nworkers * sizeof(srn_thread_t));
893
894 for (size_t i = 0; i < nworkers; i++) {
895 srn_worker_t *w = &sched->workers[i];
896 w->sched = sched;
897 w->id = i;
898 w->current = nullptr;
899 // The deque indices start empty. Its ring slots are written before they are
900 // read, and the worker's loop is set up by worker_main on its own os
901 // thread.
902 atomic_init(&w->top, 0);
903 atomic_init(&w->bottom, 0);
904 }
905
906 // Publish the coordination state before any os thread starts. `nworkers` must
907 // be set first so the quiescence check counts the right total, and the state
908 // must be RUNNING before an os thread can observe it. `run_active` was
909 // claimed at the top of this call; shutdown reads it to see a run in flight.
910 sched->idle = 0;
911 sched->nworkers = nworkers;
912 atomic_store(&sched->state, SRN_SCHED_RUNNING);
913
914 // Bring the reactor up with one channel per worker before any worker starts,
915 // so a fiber's first IO has a channel to submit on. The notify seam wakes the
916 // worker that owns the channel a completion lands on.
918
919 // Spawn nworkers - 1 os threads. The calling os thread runs worker 0 inline.
920 // A spawn failure at startup is fatal, a partial pool would never reach `idle
921 // == nworkers` and so never quiesce.
922 for (size_t i = 1; i < nworkers; i++) {
923 if (srn_thread_spawn(&sched->os_threads[i], worker_main, &sched->workers[i]) != SRN_THREAD_OK) {
924 PANIC("failed to spawn an os thread");
925 }
926 }
927
928 worker_main(&sched->workers[0]);
929
930 // Worker 0 has stopped, so the run is over from the caller's point of view.
931 // The spawned os threads may still be winding down, so they are NOT joined
932 // here. `srn_sched_shutdown` joins them (the `os_threads` live on the
933 // scheduler) as part of tearing the subsystem down. Clearing `run_active`
934 // lets shutdown proceed. The state stays STOPPING, which keeps any os thread
935 // still looping on its way out.
936 atomic_store(&sched->run_active, false);
937}
938
940 PANIC_IF_NULL(sched);
941 // Flip RUNNING or DRAINING to STOPPING once. A drain stalled on an op that
942 // never completes must remain abortable, so stop escalates a drain rather
943 // than deferring to it. If the scheduler is not running, or is already
944 // stopping, there is nothing to do.
946
947 if (!atomic_compare_exchange_strong(&sched->state, &expected, SRN_SCHED_STOPPING)) {
948 expected = SRN_SCHED_DRAINING;
949 if (!atomic_compare_exchange_strong(&sched->state, &expected, SRN_SCHED_STOPPING)) {
950 return;
951 }
952 }
953
954 // Running os threads see STOPPING at the top of their next turn. Parked os
955 // threads are roused to observe it. The notify is under the lock, paired with
956 // the park path, so no wakeup is lost.
957 srn_mutex_lock(&sched->lock);
958 srn_cond_notify_all(&sched->work);
959 srn_mutex_unlock(&sched->lock);
960 SCHED_LOG("stop requested");
961}
962
964 PANIC_IF_NULL(sched);
965 // Begin a graceful winddown, only a RUNNING scheduler can enter DRAINING.
966 // Already draining or stopping, or not running at all, leaves the state as
967 // is.
969
970 if (!atomic_compare_exchange_strong(&sched->state, &expected, SRN_SCHED_DRAINING)) {
971 return;
972 }
973
974 // From here `srn_sched_accepting_submissions` returns false, so the next IO a
975 // fiber attempts is fenced into a cancelled completion and the fiber unwinds
976 // rather than parking on a fresh op. Workers do NOT break on DRAINING, so
977 // every runnable fiber still runs and every in-flight op still completes; the
978 // pool converges to the same quiescence as a natural finish, which then moves
979 // the state to STOPPING. A never-completing in-flight op (an idle recv, a
980 // long sleep) stalls this until it finishes -- bounding that needs op CANCEL
981 // (E1) and is out of scope here.
982 //
983 // The notify wakes any os thread already parked so it re-checks state. A
984 // worker parked on outstanding IO simply re-parks (DRAINING keeps it
985 // parking), which is harmless.
986 srn_mutex_lock(&sched->lock);
987 srn_cond_notify_all(&sched->work);
988 srn_mutex_unlock(&sched->lock);
989 SCHED_LOG("drain requested");
990}
991
992// -----------------------------------------------------------------------------
993// Fiber-facing operations
994// -----------------------------------------------------------------------------
995// yield = switch to the loop, which re-enqueues self once the context is saved
996// suspend = switch to the loop, which then runs the commit to publish the
997// parked fiber to its waker
998// ready = enqueue(a named fiber), without switching
999
1003
1004 // Switch to the worker's loop without enqueuing first. The worker routine
1005 // puts this fiber back on the ready queue once the switch has saved its
1006 // context. Enqueuing here, before the switch, would let another os thread
1007 // dequeue and resume the fiber while this os thread is still saving its
1008 // context -- two os threads on one fiber stack, which corrupts the switch.
1009 srn_fiber_t *self = worker->current;
1010 PANIC_IF_NULL(self);
1011 srn_fiber_switch(self, &worker->loop);
1012}
1013
1014/// A suspended fiber is on no scheduler queue, and the scheduler does not track
1015/// what it waits on -- whoever wakes it does. The `commit` callback runs on the
1016/// worker's loop side once the fiber has switched out. It hands the fiber's
1017/// pointer to the event source it blocks on (a peer fiber, a lock's waiter
1018/// list, the IO reactor's fd table), so that party can call srn_fiber_ready
1019/// when the awaited event occurs. Running commit only after the suspend
1020/// completes is what makes the hand-off race free, a waker can never observe a
1021/// half-suspended fiber. If commit registers the fiber nowhere, it is genuinely
1022/// lost -- a deadlock, like an os thread blocking on a condition nobody
1023/// signals.
1024void srn_fiber_suspend(srn_fiber_park_fn commit, void *arg) {
1025 PANIC_IF_NULL(commit);
1026
1029
1030 srn_fiber_t *self = worker->current;
1031 PANIC_IF_NULL(self);
1032
1033 // The fiber carries its own commit. The worker routine runs it after we
1034 // switch out -- the one safe point to publish a fully suspended fiber to its
1035 // waker. The routine also stamps the `SUSPENDED` state once the switch
1036 // completes, so the `state` never marks a fiber that is still suspending.
1037 // This call leaves the `state` as `RUNNING` and lets the switch carry the
1038 // fiber off the os thread.
1039 self->park_commit = commit;
1040 self->park_arg = arg;
1041 srn_fiber_switch(self, &worker->loop);
1042}
1043
1045 PANIC_IF_NULL(fiber);
1046
1047 // Wake a suspended fiber. The flip in `ready_fiber` lets exactly one of
1048 // several racing wakers enqueue it (an IO completion and a timeout firing on
1049 // it, say), while the rest find it no longer `SUSPENDED` and do nothing. The
1050 // scheduler is resolved from the fiber, not the calling os thread, so the
1051 // reactor -- the one legitimate waker outside the worker pool, since
1052 // quiescence accounts for its in-flight ops -- can wake it too. An
1053 // unrelated os thread must not, its pending wake is invisible to
1054 // quiescence, so the run can end before the wake arrives.
1056}
1057
1059 return current_worker != nullptr ? current_worker->current : nullptr;
1060}
1061
1066
1067/// Add the calling fiber to the target's waiter list and stay parked, unless
1068/// the target has already finished, in which case decline to park so the caller
1069/// resumes at once. The DONE check and the list insert run together under the
1070/// global lock, which also guards the list against the DONE handler that drains
1071/// it. So this either sees the target finished and declines, or joins the list
1072/// before the drain and is woken by it, never lost in between.
1073static bool wait_for_park(srn_fiber_t *self, void *arg) {
1074 srn_fiber_t *target = arg;
1076
1077 srn_mutex_lock(&sched->lock);
1078
1079 if (target->state == SRN_FIBER_DONE) {
1080 srn_mutex_unlock(&sched->lock);
1081 return false;
1082 }
1083
1084 self->link = target->waiters;
1085 target->waiters = self;
1086
1087 srn_mutex_unlock(&sched->lock);
1088 return true;
1089}
1090
1092 PANIC_IF_NULL(target);
1093 PANIC_IF(target == srn_fiber_current(), "srn_fiber_wait_for: a fiber cannot wait for itself");
1094
1095 // Suspend until the target finishes (wait_for_park registers us on its waiter
1096 // list). The target's DONE handling in the worker routine wakes us. The
1097 // result is read from the struct, which survives the target's reap.
1099 return target->result;
1100}
1101
1103 // We use the SIZE_MAX as an idicator that there is no current
1104 // worker for the running os thread. (size_t)-1 == SIZE_MAX
1105 return current_worker == nullptr ? (srn_worker_id_t)-1 : current_worker->id;
1106}
1107
1109 PANIC_IF_NULL(sched);
1110 // Only a RUNNING scheduler takes new IO. Once DRAINING or STOPPING, the IO
1111 // bridge fences submissions so fibers unwind instead of parking on ops the
1112 // wind-down would have to wait out.
1113 return atomic_load(&sched->state) == SRN_SCHED_RUNNING;
1114}
1115
1116void srn_sched_wake_worker(srn_scheduler_t *sched, size_t channel) {
1117 // TODO(lxsameer): Wake up the worker in charge of the given channel. instead
1118 // of waking all.
1119 UNUSED(channel);
1120 srn_mutex_lock(&sched->lock);
1121 srn_cond_notify_all(&sched->work);
1122 srn_mutex_unlock(&sched->lock);
1123}
static srn_fiber_result_t worker(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:44
static srn_fiber_result_t waiter(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:51
#define SRN_MAX_WORKERS
The absolute worker ceiling.
void srn_mm_free(srn_mm_t *mm, void *ptr)
Release a pointer previously returned by srn_mm_malloc or srn_mm_reallocate.
Definition default.c:165
void * srn_mm_malloc(srn_mm_t *mm, size_t size)
Generic allocations that do not participate in the block based pools.
Definition default.c:155
void srn_fiber_init_thread(srn_fiber_t *f)
Represent the calling OS thread as the running fiber ("#0"), so the scheduler or a test can switch aw...
Definition fiber.c:154
void srn_fiber_switch(srn_fiber_t *from, srn_fiber_t *to)
Compiled without AddressSanitizer instrumentation, in stack-use-after-return mode ASan would place fr...
Definition fiber.c:66
void srn_fiber_on_reap(srn_fiber_t *fiber)
Call when a finished fiber is reaped, after it has switched away for the last time.
Definition fiber.c:133
AI Generated (🤦) Fiber subsystem overview.
#define srn_fiber_get_scheduler_m(fiber)
Definition fiber.h:159
size_t srn_worker_id_t
Definition fiber.h:149
void srn_fiber_stack_free(srn_fiber_stack_t stack)
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:232
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:236
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:234
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:240
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:238
bool(* srn_fiber_park_fn)(srn_fiber_t *self, void *arg)
Suspend commit callback.
Definition fiber.h:262
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
Definition fiber.h:157
enum srn_fiber_state_e srn_fiber_state_t
#define srn_mm_immortal_allocate(mm, T)
Definition interface.h:183
void srn_reactor_consume(srn_reactor_t *reactor, size_t channel)
Runs on the worker loop who owns the channel.
Definition io.c:121
void srn_reactor_activate(srn_reactor_t *reactor, size_t nchannels, srn_reactor_notify_fn notify)
Bring the reactor up, allocate nchannels channels (one per worker) and start the reactor thread.
Definition reactor.c:390
bool srn_reactor_idle(srn_reactor_t *reactor)
Whether the reactor has no operations in flight.
Definition reactor.c:169
bool srn_reactor_channel_has_completions(srn_reactor_t *reactor, size_t channel)
Whether channel's completion queue has unconsumed completions.
Definition reactor.c:237
Reactor overview.
void srn_sched_register(srn_scheduler_t *sched, srn_fiber_t *fiber)
Record a fiber in the scheduler's registry of live fibers, where it stays until it is reaped.
Definition scheduler.c:318
srn_fiber_t * srn_fiber_worker_loop(void)
The worker's loop of the worker running on the calling os thread.
Definition scheduler.c:1062
static void registry_add(srn_scheduler_t *sched, srn_fiber_t *fiber)
Insert at the head of the registry. Caller must hold sched->lock.
Definition scheduler.c:290
#define SCHED_LOG(FMT,...)
Definition scheduler.c:29
static void ready_fiber(srn_scheduler_t *sched, srn_fiber_t *fiber)
Wake a parked fiber by flipping SUSPENDED to READY and enqueuing it.
Definition scheduler.c:650
void srn_sched_wake_worker(srn_scheduler_t *sched, size_t channel)
Rouse parked workers so the owner of channel consumes its completions.
Definition scheduler.c:1116
void srn_fiber_ready(srn_fiber_t *fiber)
Mark a suspended fiber runnable again, waking it when the event it awaited occurs.
Definition scheduler.c:1044
static void worker_run(srn_worker_t *worker)
Run the worker routine over worker on the calling os thread.
Definition scheduler.c:698
srn_worker_id_t srn_sched_current_worker_id()
Return the id of the worker that the calling os thread is running, or SIZE_MAX when the calling threa...
Definition scheduler.c:1102
srn_fiber_result_t srn_fiber_wait_for(srn_fiber_t *target)
Block the calling fiber until target finishes, then return its result.
Definition scheduler.c:1091
void srn_sched_drain(srn_scheduler_t *sched)
Ask a running scheduler to wind down gracefully.
Definition scheduler.c:963
static srn_fiber_t * local_pop(srn_worker_t *w)
Owner only.
Definition scheduler.c:473
static _Thread_local srn_worker_t * current_worker
The worker the calling os thread is running, or null when this os thread is not running the worker ro...
Definition scheduler.c:251
void srn_sched_shutdown(srn_scheduler_t *sched)
The one stop tear down of the fiber subsystem, should be called once srn_sched_run has returned.
Definition scheduler.c:327
static bool local_push(srn_worker_t *w, srn_fiber_t *fiber)
This operation is only for the owner of the ring.
Definition scheduler.c:444
static void registry_remove(srn_scheduler_t *sched, srn_fiber_t *fiber)
Unlink from the registry.
Definition scheduler.c:303
bool srn_sched_accepting_submissions(srn_scheduler_t *sched)
Whether the scheduler still accepts new IO submissions.
Definition scheduler.c:1108
static void push_ready(srn_scheduler_t *sched, srn_fiber_t *fiber)
Put a runnable fiber on a queue, with its state already set to READY.
Definition scheduler.c:599
#define SRN_FIBER_LOCAL_RING_CAP
Capacity of each worker's local work-stealing deque.
Definition scheduler.c:217
void srn_sched_stop(srn_scheduler_t *sched)
Ask a running scheduler to stop.
Definition scheduler.c:939
void srn_sched_enqueue(srn_scheduler_t *sched, srn_fiber_t *fiber)
Place a fiber on a scheduler's ready queue, making it eligible to run.
Definition scheduler.c:622
static void announce_work(srn_scheduler_t *sched)
Wake the os thread of one parked worker after a fiber has joined a queue.
Definition scheduler.c:420
srn_scheduler_t * srn_sched_init(srn_engine_t *engine)
Definition scheduler.c:257
srn_fiber_t * srn_fiber_current(void)
The fiber currently running on this os thread, or null when the calling thread is not a worker or the...
Definition scheduler.c:1058
static void worker_main(void *arg)
The entry an os thread starts in.
Definition scheduler.c:837
srn_sched_state_t
The scheduler's lifecycle as one atomic value.
Definition scheduler.c:130
@ SRN_SCHED_RUNNING
Definition scheduler.c:132
@ SRN_SCHED_STOPPING
Definition scheduler.c:134
@ SRN_SCHED_IDLE
Definition scheduler.c:131
@ SRN_SCHED_DRAINING
Definition scheduler.c:133
void srn_fiber_schedule(srn_fiber_t *fiber)
Schedule a NEW fiber, making it eligible to run.
Definition scheduler.c:630
static srn_fiber_t * global_take(srn_scheduler_t *sched)
Pop the head of the global queue, or null when empty.
Definition scheduler.c:578
static void global_enqueue(srn_scheduler_t *sched, srn_fiber_t *fiber)
Append a fiber to the global/overflow queue.
Definition scheduler.c:552
static srn_fiber_t * find_work(srn_worker_t *w)
Find a fiber to run, the worker's own deque first, then the global queue, then a steal of one fiber f...
Definition scheduler.c:661
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Definition scheduler.c:843
static bool wait_for_park(srn_fiber_t *self, void *arg)
Add the calling fiber to the target's waiter list and stay parked, unless the target has already fini...
Definition scheduler.c:1073
void srn_fiber_suspend(srn_fiber_park_fn commit, void *arg)
A suspended fiber is on no scheduler queue, and the scheduler does not track what it waits on – whoev...
Definition scheduler.c:1024
void srn_fiber_yield(void)
Yield cooperatively, re-enqueue the running fiber and run the next ready one.
Definition scheduler.c:1000
#define SCHED_TRACE(...)
Per-operation deque and queue tracing (push, pop, steal, wake).
Definition scheduler.c:38
static srn_fiber_t * local_steal(srn_worker_t *victim)
Thief side.
Definition scheduler.c:515
Every runtime knob, in one place.
srn_fiber_config_t fiber
Engine is a structure to own the long living and main pieces of the compiler.
Definition engine.h:51
srn_configuration_t config
The runtime's tunable knobs, the single source for every configurable value (see configuration....
Definition engine.h:62
srn_mm_t * mm
Memory manager.
Definition engine.h:65
srn_reactor_t * reactor
The I/O reactor, that is in charge of handling everything I/O.
Definition engine.h:78
size_t workers
Worker count used when a run does not specify one.
size_t max_workers
Hard ceiling a requested worker count is clamped to.
char name[SRN_FIBER_NAME_MAX]
Debug name, the caller's choice copied at creation, or the autogenerated f#<id> from the engine wide ...
Definition fiber.h:330
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:273
srn_fiber_t * link
Intrusive link threading this fiber onto one of the scheduler's singly-linked lists (the ready run qu...
Definition fiber.h:303
srn_fiber_t * waiters
Head of the list of fibers blocked in srn_fiber_wait_for on this fiber.
Definition fiber.h:309
void * park_arg
Definition fiber.h:285
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:279
srn_fiber_park_fn park_commit
While this fiber is suspending, the commit the worker routine runs once the fiber is off the stack,...
Definition fiber.h:284
srn_fiber_stack_t stack
Definition fiber.h:267
srn_fiber_t * reg_prev
Registry links.
Definition fiber.h:322
srn_fiber_t * reg_next
Definition fiber.h:323
atomic_size_t runnable
Definition scheduler.c:183
bool destroyed
Set once srn_sched_shutdown has torn the scheduler down.
Definition scheduler.c:209
srn_engine_t * engine
Definition scheduler.c:138
_Atomic bool run_active
True for the duration of an srn_sched_run call.
Definition scheduler.c:205
srn_fiber_t * registry
Registry, head of the doubly-linked list (through reg_prev/reg_next) of every live fiber,...
Definition scheduler.c:156
atomic_size_t idle
Definition scheduler.c:182
srn_mutex_t lock
Global lock.
Definition scheduler.c:144
srn_cond_t work
Worker coordination.
Definition scheduler.c:181
srn_fiber_t * ready_head
Global / overflow queue.
Definition scheduler.c:149
srn_thread_t * os_threads
Definition scheduler.c:200
srn_fiber_t * ready_tail
Definition scheduler.c:150
_Atomic srn_sched_state_t state
Definition scheduler.c:186
srn_worker_t * workers
srn_sched_run allocates these two arrays and srn_sched_shutdown frees them.
Definition scheduler.c:199
The state one os thread uses to run fibers.
Definition scheduler.c:228
atomic_intptr_t top
Chase-Lev deque.
Definition scheduler.c:239
srn_fiber_t * current
Definition scheduler.c:231
srn_scheduler_t * sched
Definition scheduler.c:229
atomic_intptr_t bottom
Definition scheduler.c:240
srn_fiber_t loop
Definition scheduler.c:230
srn_worker_id_t id
Definition scheduler.c:232
srn_thread_t, srn_mutex_t, and srn_cond_t model the thread-level operations the runtime needs,...
srn_thread_status_t srn_mutex_destroy(srn_mutex_t *m)
Release a mutex's resources.
srn_thread_status_t srn_mutex_init(srn_mutex_t *m)
srn_thread_status_t srn_cond_destroy(srn_cond_t *c)
Release a condition's resources.
srn_thread_status_t srn_thread_join(srn_thread_t *t)
Block until the thread started for t returns.
srn_thread_status_t srn_mutex_unlock(srn_mutex_t *m)
@ SRN_THREAD_OK
Definition thread.h:62
srn_thread_status_t srn_cond_wait(srn_cond_t *c, srn_mutex_t *m)
Release m, sleep until notified, then re-acquire m before returning.
srn_thread_status_t srn_mutex_lock(srn_mutex_t *m)
srn_thread_status_t srn_cond_init(srn_cond_t *c)
srn_thread_status_t srn_cond_notify_one(srn_cond_t *c)
Wake one waiter.
srn_thread_status_t srn_cond_notify_all(srn_cond_t *c)
Wake every waiter.
srn_thread_status_t srn_thread_spawn(srn_thread_t *t, void(*fn)(void *), void *arg)
Run fn(arg) on a new OS thread.
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
#define PANIC_IF(cond, msg)
Definition utils.h:59
#define UNUSED(x)
Definition utils.h:45
#define PANIC(msg)
Definition utils.h:53