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