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