Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
scheduler.c File Reference
#include <inttypes.h>
#include <stdatomic.h>
#include <stdio.h>
#include "serene/rt/context.h"
#include "serene/rt/engine.h"
#include "serene/rt/fiber.h"
#include "serene/rt/fiber/thread.h"
#include "serene/rt/mm/interface.h"
#include "serene/rt/reactor.h"
#include "serene/utils.h"
Include dependency graph for scheduler.c:

Go to the source code of this file.

Data Structures

struct  srn_scheduler_t
struct  srn_worker_t
 The state one os thread uses to run fibers. More...

Macros

#define SCHED_LOG(FMT, ...)
#define SCHED_TRACE(...)
 Per-operation deque and queue tracing (push, pop, steal, wake).
#define SRN_FIBER_LOCAL_RING_CAP   256
 Capacity of each worker's local work-stealing deque.

Typedefs

typedef struct srn_worker_t srn_worker_t
 Defined here, not in fiber.h, which only forward declares srn_scheduler_t.
typedef enum srn_sched_state_t srn_sched_state_t
 The scheduler's lifecycle as one atomic value.

Enumerations

enum  srn_sched_state_t { SRN_SCHED_IDLE , SRN_SCHED_RUNNING , SRN_SCHED_DRAINING , SRN_SCHED_STOPPING }
 The scheduler's lifecycle as one atomic value. More...

Functions

srn_scheduler_tsrn_sched_init (srn_engine_t *engine)
static void registry_add (srn_scheduler_t *sched, srn_fiber_t *fiber)
 Insert at the head of the registry. Caller must hold sched->lock.
static void registry_remove (srn_scheduler_t *sched, srn_fiber_t *fiber)
 Unlink from the registry.
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.
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.
static void announce_work (srn_scheduler_t *sched)
 Wake the os thread of one parked worker after a fiber has joined a queue.
static bool local_push (srn_worker_t *w, srn_fiber_t *fiber)
 This operation is only for the owner of the ring.
static srn_fiber_tlocal_pop (srn_worker_t *w)
 Owner only.
static srn_fiber_tlocal_steal (srn_worker_t *victim)
 Thief side.
static void global_enqueue (srn_scheduler_t *sched, srn_fiber_t *fiber)
 Append a fiber to the global/overflow queue.
static srn_fiber_tglobal_take (srn_scheduler_t *sched)
 Pop the head of the global queue, or null when empty.
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.
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.
void srn_fiber_schedule (srn_fiber_t *fiber)
 Schedule a NEW fiber, making it eligible to run.
static void ready_fiber (srn_scheduler_t *sched, srn_fiber_t *fiber)
 Wake a parked fiber by flipping SUSPENDED to READY and enqueuing it.
static srn_fiber_tfind_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 from each peer in turn.
static void worker_run (srn_worker_t *worker)
 Run the worker routine over worker on the calling os thread.
static void worker_main (void *arg)
 The entry an os thread starts in.
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 os thread parked on an empty queue) or a stop is requested with srn_sched_stop.
void srn_sched_stop (srn_scheduler_t *sched)
 Ask a running scheduler to stop.
void srn_sched_drain (srn_scheduler_t *sched)
 Ask a running scheduler to wind down gracefully.
void srn_fiber_yield (void)
 Yield cooperatively, re-enqueue the running fiber and run the next ready one.
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 – whoever wakes it does.
void srn_fiber_ready (srn_fiber_t *fiber)
 Mark a suspended fiber runnable again, waking it when the event it awaited occurs.
srn_fiber_tsrn_fiber_current (void)
 The fiber currently running on this os thread, or null when the calling thread is not a worker or the worker is running its own loop rather than a fiber.
srn_fiber_tsrn_fiber_worker_loop (void)
 The worker's loop of the worker running on the calling os thread.
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 finished, in which case decline to park so the caller resumes at once.
srn_fiber_result_t srn_fiber_wait_for (srn_fiber_t *target)
 Block the calling fiber until target finishes, then return its result.
void srn_fiber_autoname (srn_engine_t *engine, char *dst, size_t size)
 Write the autogenerated debug name for a new fiber into dst.
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 thread is not a worker.
bool srn_sched_accepting_submissions (srn_scheduler_t *sched)
 Whether the scheduler still accepts new IO submissions.
void srn_sched_wake_worker (srn_scheduler_t *sched, size_t channel)
 Rouse parked workers so the owner of channel consumes its completions.

Variables

static _Thread_local srn_worker_tcurrent_worker = nullptr
 The worker the calling os thread is running, or null when this os thread is not running the worker routine (srn_sched_run is not active on it).

Macro Definition Documentation

◆ SCHED_LOG

#define SCHED_LOG ( FMT,
... )
Value:
DBG("SCHED", FMT __VA_OPT__(, ) __VA_ARGS__)
#define DBG(...)
Definition utils.h:179

Definition at line 31 of file scheduler.c.

◆ SCHED_TRACE

#define SCHED_TRACE ( ...)
Value:
do { \
} while (0)

Per-operation deque and queue tracing (push, pop, steal, wake).

This fires on the hot path, so it floods a debug build and is off unless SRN_SCHED_TRACE is defined. SCHED_LOG (scheduler lifecycle, stop, shutdown reap) stays on in a debug build. Both are silent in release, since DBG is.

Definition at line 40 of file scheduler.c.

40# define SCHED_TRACE(...) \
41 do { \
42 } while (0)

◆ SRN_FIBER_LOCAL_RING_CAP

#define SRN_FIBER_LOCAL_RING_CAP   256

Capacity of each worker's local work-stealing deque.

Must be a power of two: the live slot for a deque index is index & (cap - 1). 256 matches the common choice (Go, Tokio). A fiber that does not fit overflows to the global queue. This is the single source of truth for the size, so a configuration layer can later drive it.

Definition at line 219 of file scheduler.c.

Typedef Documentation

◆ srn_sched_state_t

The scheduler's lifecycle as one atomic value.

RUNNING means a run is servicing the queues. DRAINING is the graceful wind down set by srn_sched_drain, workers keep running every runnable fiber and let in-flight IO finish, but new IO submissions are fenced (see srn_sched_accepting_submissions) so fibers unwind instead of parking on fresh ops, and the pool converges to quiescence. STOPPING tells the workers to wind down at once, set at natural quiescence or abruptly by srn_sched_stop. IDLE is the resting state before a run starts. Workers read it without the lock, so it is atomic.

The order matters, a state at or past DRAINING no longer accepts new IO, which srn_sched_accepting_submissions relies on.

◆ srn_worker_t

typedef struct srn_worker_t srn_worker_t

Defined here, not in fiber.h, which only forward declares srn_scheduler_t.

Consumers hold a srn_scheduler_t * and never see the layout. Two reasons:

  1. The layout can evolve without recompiling or disturbing consumers. Going M:N this struct grows per-thread local queues, a worker array, a reactor handle, steal state – none of which should ripple into every translation unit that includes fiber.h.
  2. It makes the decide/execute boundary physical. The scheduler owns the ready queue and the picking policy. The worker routine and fibers must reach it only through enqueue/yield/ready. Keeping the fields private means no caller can poke the queue directly – the encapsulation is the contract, enforced by the compiler rather than by convention.

Definition at line 118 of file scheduler.c.

Enumeration Type Documentation

◆ srn_sched_state_t

The scheduler's lifecycle as one atomic value.

RUNNING means a run is servicing the queues. DRAINING is the graceful wind down set by srn_sched_drain, workers keep running every runnable fiber and let in-flight IO finish, but new IO submissions are fenced (see srn_sched_accepting_submissions) so fibers unwind instead of parking on fresh ops, and the pool converges to quiescence. STOPPING tells the workers to wind down at once, set at natural quiescence or abruptly by srn_sched_stop. IDLE is the resting state before a run starts. Workers read it without the lock, so it is atomic.

The order matters, a state at or past DRAINING no longer accepts new IO, which srn_sched_accepting_submissions relies on.

Enumerator
SRN_SCHED_IDLE 
SRN_SCHED_RUNNING 
SRN_SCHED_DRAINING 
SRN_SCHED_STOPPING 

Definition at line 132 of file scheduler.c.

132 {
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

Function Documentation

◆ announce_work()

void announce_work ( srn_scheduler_t * sched)
static

Wake the os thread of one parked worker after a fiber has joined a queue.

"Parked" means that os thread is asleep in srn_cond_wait because it found no runnable fiber anywhere. This is not a fiber suspending. It is the whole os thread blocked, and the notify wakes it so it looks again.

runnable is bumped first, then idle is read. Paired against the park path, which bumps idle then reads runnable, this ordering means the two sides can never both miss, so a wakeup is never lost. The notify takes the global lock (the condition's lock) but only when an os thread is actually parked, so the common busy case never touches it.

WARNING: this runs with NO lock around the runnable++ and the idle read, so its only tie to the park path is the seq_cst total order. Both must stay seq_cst. RELAX EITHER AND THE WAKEUP CAN BE LOST (an os thread parked with a runnable fiber queued). See the srn_scheduler_t coordination comment for the full reasoning.

Definition at line 427 of file scheduler.c.

427 {
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}
#define SCHED_TRACE(...)
Per-operation deque and queue tracing (push, pop, steal, wake).
Definition scheduler.c:40
atomic_size_t runnable
Definition scheduler.c:185
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_thread_status_t srn_mutex_unlock(srn_mutex_t *m)
srn_thread_status_t srn_mutex_lock(srn_mutex_t *m)
srn_thread_status_t srn_cond_notify_one(srn_cond_t *c)
Wake one waiter.
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
Here is the call graph for this function:
Here is the caller graph for this function:

◆ find_work()

srn_fiber_t * find_work ( srn_worker_t * w)
static

Find a fiber to run, the worker's own deque first, then the global queue, then a steal of one fiber from each peer in turn.

Null when nothing is runnable anywhere this worker can reach. Decrements runnable for whatever it takes.

Definition at line 669 of file scheduler.c.

669 {
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}
#define FIBER_TRACEPOINT(...)
Definition fiber.h:146
static srn_fiber_t * local_pop(srn_worker_t *w)
Owner only.
Definition scheduler.c:480
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 srn_fiber_t * local_steal(srn_worker_t *victim)
Thief side.
Definition scheduler.c:522
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
srn_scheduler_t * sched
Definition scheduler.c:231
srn_worker_id_t id
Definition scheduler.c:234
Here is the call graph for this function:
Here is the caller graph for this function:

◆ global_enqueue()

void global_enqueue ( srn_scheduler_t * sched,
srn_fiber_t * fiber )
static

Append a fiber to the global/overflow queue.

The caller has set its state. The push, the runnable bump, and the wake all run under the global lock, so this path is trivially serialized against the park path and needs no separate ordering argument.

Put a fiber on the global queue and wake a parked os thread if any. Unlike announce_work, the runnable++, the idle read, and the notify all happen under the lock, so this path is safe by mutual exclusion and does not lean on the seq_cst ordering the lockless path does.

Definition at line 559 of file scheduler.c.

559 {
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}
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 * ready_head
Global / overflow queue.
Definition scheduler.c:151
srn_fiber_t * ready_tail
Definition scheduler.c:152
Here is the call graph for this function:
Here is the caller graph for this function:

◆ global_take()

srn_fiber_t * global_take ( srn_scheduler_t * sched)
static

Pop the head of the global queue, or null when empty.

The runnable adjustment is left to find_work, the only taker.

Definition at line 585 of file scheduler.c.

585 {
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}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ local_pop()

srn_fiber_t * local_pop ( srn_worker_t * w)
static

Owner only.

Pop a fiber from the bottom, or null when empty. The seq_cst fence and the compare-and-swap settle the race with a thief over the last element.

Definition at line 480 of file scheduler.c.

480 {
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}
#define SRN_FIBER_LOCAL_RING_CAP
Capacity of each worker's local work-stealing deque.
Definition scheduler.c:219
atomic_intptr_t top
Chase-Lev deque.
Definition scheduler.c:245
atomic_intptr_t bottom
Definition scheduler.c:246
Here is the caller graph for this function:

◆ local_push()

bool local_push ( srn_worker_t * w,
srn_fiber_t * fiber )
static

This operation is only for the owner of the ring.

Push a fiber on the bottom. Returns false when the deque is full, so the caller can overflow it to the global queue. The caller has set state.

Definition at line 451 of file scheduler.c.

451 {
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}
Here is the caller graph for this function:

◆ local_steal()

srn_fiber_t * local_steal ( srn_worker_t * victim)
static

Thief side.

Take a fiber from victim's top, or null when the deque is empty or a concurrent take won the race – the caller then just moves to the next victim.

Definition at line 522 of file scheduler.c.

522 {
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}
Here is the caller graph for this function:

◆ push_ready()

void push_ready ( srn_scheduler_t * sched,
srn_fiber_t * fiber )
static

Put a runnable fiber on a queue, with its state already set to READY.

A fiber enqueued while running on a worker goes onto that worker's local deque, keeping its work local. One enqueued from off a worker (the initial fibers made before the run, or an external waker), or one that does not fit a full local deque, goes to the global queue.

Definition at line 606 of file scheduler.c.

606 {
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}
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
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 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
static void global_enqueue(srn_scheduler_t *sched, srn_fiber_t *fiber)
Append a fiber to the global/overflow queue.
Definition scheduler.c:559
Here is the call graph for this function:
Here is the caller graph for this function:

◆ ready_fiber()

void ready_fiber ( srn_scheduler_t * sched,
srn_fiber_t * fiber )
static

Wake a parked fiber by flipping SUSPENDED to READY and enqueuing it.

Only the flip's winner enqueues, so racing wakers cannot double-enqueue it, and a fiber that is not parked is left untouched. The scheduler does not check the awaited condition. A fiber woken early resumes, re-checks, and parks again.

Definition at line 658 of file scheduler.c.

658 {
660 if (atomic_compare_exchange_strong(&fiber->state, &expected, SRN_FIBER_READY)) {
661 push_ready(sched, fiber);
662 }
663}
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:238
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:242
enum srn_fiber_state_e srn_fiber_state_t
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
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:277
Here is the call graph for this function:
Here is the caller graph for this function:

◆ registry_add()

void registry_add ( srn_scheduler_t * sched,
srn_fiber_t * fiber )
static

Insert at the head of the registry. Caller must hold sched->lock.

Definition at line 296 of file scheduler.c.

296 {
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}
srn_fiber_t * reg_prev
Registry links.
Definition fiber.h:326
srn_fiber_t * reg_next
Definition fiber.h:327
srn_fiber_t * registry
Registry, head of the doubly-linked list (through reg_prev/reg_next) of every live fiber,...
Definition scheduler.c:158
Here is the caller graph for this function:

◆ registry_remove()

void registry_remove ( srn_scheduler_t * sched,
srn_fiber_t * fiber )
static

Unlink from the registry.

O(1), thanks to the back pointer. Caller must hold sched->lock.

Definition at line 309 of file scheduler.c.

309 {
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}
Here is the caller graph for this function:

◆ srn_fiber_autoname()

void srn_fiber_autoname ( srn_engine_t * engine,
char * dst,
size_t size )

Write the autogenerated debug name for a new fiber into dst.

Created on a worker the tag is f#<worker>:<n>, the creating worker's id and its spawn count. Created off the pool it is f#m:<id> from the engine wide object id counter. The worker part is provenance, not placement, a stolen fiber runs anywhere. srn_fiber_make uses this when given no name.

Definition at line 1141 of file scheduler.c.

1141 {
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}
srn_object_id_t srn_allocate_object_id(srn_engine_t *engine)
Definition engine.c:172
uint64_t spawned
Count of fibers this worker has created so far.
Definition scheduler.c:238
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_current()

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 worker is running its own loop rather than a fiber.

Definition at line 1097 of file scheduler.c.

1097 {
1098 return current_worker != nullptr ? current_worker->current : nullptr;
1099}
Here is the caller graph for this function:

◆ srn_fiber_ready()

void srn_fiber_ready ( srn_fiber_t * fiber)

Mark a suspended fiber runnable again, waking it when the event it awaited occurs.

Callable from a fiber, a worker, or the reactor. The reactor is the only legitimate waker outside the worker pool.

Definition at line 1083 of file scheduler.c.

1083 {
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}
#define srn_fiber_get_scheduler_m(fiber)
Definition fiber.h:163
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_schedule()

void srn_fiber_schedule ( srn_fiber_t * fiber)

Schedule a NEW fiber, making it eligible to run.

A fiber is scheduled exactly once, scheduling one that is not NEW panics, and waking a suspended fiber is srn_fiber_ready's job instead.

Definition at line 638 of file scheduler.c.

638 {
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}
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:236
#define PANIC_IF(cond, msg)
Definition utils.h:59
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_suspend()

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 – whoever wakes it does.

Park the running fiber until a party calls srn_fiber_ready.

The commit callback runs on the worker's loop side once the fiber has switched out. It hands the fiber's pointer to the event source it blocks on (a peer fiber, a lock's waiter list, the IO reactor's fd table), so that party can call srn_fiber_ready when the awaited event occurs. Running commit only after the suspend completes is what makes the hand-off race free, a waker can never observe a half-suspended fiber. If commit registers the fiber nowhere, it is genuinely lost – a deadlock, like an os thread blocking on a condition nobody signals.

Definition at line 1063 of file scheduler.c.

1063 {
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}
static srn_fiber_result_t worker(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:44
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 * park_arg
Definition fiber.h:289
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_wait_for()

srn_fiber_result_t srn_fiber_wait_for ( srn_fiber_t * target)

Block the calling fiber until target finishes, then return its result.

If the target is already done, returns immediately.

Definition at line 1130 of file scheduler.c.

1130 {
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}
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 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_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
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:283
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_worker_loop()

srn_fiber_t * srn_fiber_worker_loop ( void )

The worker's loop of the worker running on the calling os thread.

The fiber that resumes when the current fiber yields, suspends, or finishes, the resumer a fiber's launcher hands control back to. Each worker has its own, so this is valid only on an os thread that is currently running the worker routine.

Definition at line 1101 of file scheduler.c.

1101 {
1103 return &current_worker->loop;
1104}
Here is the caller graph for this function:

◆ srn_fiber_yield()

void srn_fiber_yield ( void )

Yield cooperatively, re-enqueue the running fiber and run the next ready one.

Acts on the fiber currently running on this thread.

Definition at line 1039 of file scheduler.c.

1039 {
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}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_accepting_submissions()

bool srn_sched_accepting_submissions ( srn_scheduler_t * sched)

Whether the scheduler still accepts new IO submissions.

True only while RUNNING; false once srn_sched_drain or srn_sched_stop has begun winding the pool down. The IO bridge reads this to fence submissions during a wind-down, so a fiber's IO call comes back cancelled instead of parking on an op the wind-down would have to wait out.

Definition at line 1163 of file scheduler.c.

1163 {
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}
_Atomic srn_sched_state_t state
Definition scheduler.c:188
Here is the caller graph for this function:

◆ srn_sched_current_worker_id()

srn_worker_id_t srn_sched_current_worker_id ( void )

Return the id of the worker that the calling os thread is running, or SIZE_MAX when the calling thread is not a worker.

Definition at line 1157 of file scheduler.c.

1157 {
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}
size_t srn_worker_id_t
Definition fiber.h:153
Here is the caller graph for this function:

◆ srn_sched_drain()

void srn_sched_drain ( srn_scheduler_t * sched)

Ask a running scheduler to wind down gracefully.

Unlike srn_sched_stop, the workers keep running every runnable fiber and let in-flight IO complete; only new IO submissions are fenced, so a fiber's next IO call returns -ECANCELED and the fiber unwinds rather than parking on a fresh op. The pool then reaches the same quiescence as a natural finish and srn_sched_run returns. An in-flight op that never completes (an idle recv, a long sleep) stalls the wind-down until it finishes; bounding that needs op cancellation and is not provided yet. Does not wait. Safe to call from any os thread or a fiber. A no-op if the scheduler is not running.

Definition at line 1002 of file scheduler.c.

1002 {
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}
#define SCHED_LOG(FMT,...)
Definition scheduler.c:31
srn_thread_status_t srn_cond_notify_all(srn_cond_t *c)
Wake every waiter.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_enqueue()

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 at line 630 of file scheduler.c.

630 {
631 PANIC_IF_NULL(sched);
632 PANIC_IF_NULL(fiber);
633
634 fiber->state = SRN_FIBER_READY;
635 push_ready(sched, fiber);
636}
Here is the call graph for this function:

◆ srn_sched_init()

srn_scheduler_t * srn_sched_init ( srn_engine_t * engine)
nodiscard

Definition at line 263 of file scheduler.c.

263 {
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}
#define srn_mm_immortal_allocate(mm, T)
Definition interface.h:186
srn_mm_t * mm
Memory manager.
Definition engine.h:65
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_thread_t * os_threads
Definition scheduler.c:202
srn_thread_status_t srn_mutex_init(srn_mutex_t *m)
@ SRN_THREAD_OK
Definition thread.h:62
srn_thread_status_t srn_cond_init(srn_cond_t *c)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_register()

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 at line 324 of file scheduler.c.

324 {
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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_run()

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 os thread parked on an empty queue) or a stop is requested with srn_sched_stop.

The calling os thread becomes worker 0, so it does not return until then. nworkers is clamped to at least 1; with 1 it is the calling os thread alone, which keeps execution single-threaded and cooperatively ordered. The spawned os threads are not joined here – srn_sched_shutdown joins them as part of tearing the subsystem down.

Definition at line 875 of file scheduler.c.

875 {
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}
#define SRN_MAX_WORKERS
The absolute worker ceiling.
void * srn_mm_malloc(srn_mm_t *mm, size_t size)
Generic allocations that do not participate in the block based pools.
Definition default.c:155
void srn_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
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
static void worker_main(void *arg)
The entry an os thread starts in.
Definition scheduler.c:866
Every runtime knob, in one place.
srn_fiber_config_t fiber
srn_configuration_t config
The runtime's tunable knobs, the single source for every configurable value (see configuration....
Definition engine.h:62
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.
srn_fiber_t * current
Definition scheduler.c:233
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_thread_spawn(srn_thread_t *t, void(*fn)(void *), void *arg)
Run fn(arg) on a new OS thread.
#define PANIC(msg)
Definition utils.h:53
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_shutdown()

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.

It joins the os threads, then releases the stack of every fiber still registered (any left unreaped, such as one suspended with no party to wake it, or one left queued when the run stopped early), and frees the scheduler's own resources. The fiber structs themselves live in context blocks and are reclaimed with those blocks, not here.

The scheduler is NOT usable after this. A later srn_sched_run panics, and a repeated srn_sched_shutdown is a no-op (so the engine can call it unconditionally even after a caller already did).

Must be called from outside the pool, never from an os thread that is running a worker nor from a fiber, and only after srn_sched_run has returned. Calling it on a live pool panics – stop the pool with srn_sched_stop, let srn_sched_run return, then shut down.

Definition at line 333 of file scheduler.c.

333 {
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);
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}
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_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
void srn_fiber_stack_free(srn_fiber_stack_t stack)
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
srn_fiber_stack_t stack
Definition fiber.h:271
srn_thread_status_t srn_mutex_destroy(srn_mutex_t *m)
Release a mutex's resources.
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.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_stop()

void srn_sched_stop ( srn_scheduler_t * sched)

Ask a running scheduler to stop.

Each worker routine checks the request at the top of each turn and stops once the fiber it is running yields or finishes, so a slice in flight is never cut mid-execution. srn_sched_run then returns. Fibers left queued stay unrun and are reclaimed by srn_sched_shutdown. Does not wait. Safe to call from any os thread or a fiber, but NOT from a signal handler. It takes the scheduler mutex and signals the condition variable, neither of which is async signal safe.

Definition at line 978 of file scheduler.c.

978 {
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}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_wake_worker()

void srn_sched_wake_worker ( srn_scheduler_t * sched,
size_t channel )

Rouse parked workers so the owner of channel consumes its completions.

The reactor's notify hook. The wake is currently a broadcast, channel names the worker that must look, but every parked worker is notified and the ones with nothing to do re-park.

Definition at line 1171 of file scheduler.c.

1171 {
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}
#define UNUSED(x)
Definition utils.h:45
Here is the call graph for this function:
Here is the caller graph for this function:

◆ wait_for_park()

bool wait_for_park ( srn_fiber_t * self,
void * arg )
static

Add the calling fiber to the target's waiter list and stay parked, unless the target has already finished, in which case decline to park so the caller resumes at once.

The DONE check and the list insert run together under the global lock, which also guards the list against the DONE handler that drains it. So this either sees the target finished and declines, or joins the list before the drain and is woken by it, never lost in between.

Definition at line 1112 of file scheduler.c.

1112 {
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}
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:244
srn_fiber_t * waiters
Head of the list of fibers blocked in srn_fiber_wait_for on this fiber.
Definition fiber.h:313
Here is the call graph for this function:
Here is the caller graph for this function:

◆ worker_main()

void worker_main ( void * arg)
static

The entry an os thread starts in.

It sets up its worker's loop – on its own os thread, so the sanitizer captures the right stack bounds – then runs the worker routine until the pool is quiescent. arg is the worker.

Definition at line 866 of file scheduler.c.

866 {
867 srn_worker_t *worker = arg;
869
870 FIBER_TRACEPOINT(sched_thread_init, worker->id);
871
873}
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
static void worker_run(srn_worker_t *worker)
Run the worker routine over worker on the calling os thread.
Definition scheduler.c:707
Here is the call graph for this function:
Here is the caller graph for this function:

◆ worker_run()

void worker_run ( srn_worker_t * worker)
static

Run the worker routine over worker on the calling os thread.

Find a fiber, run it, handle how it gave up control, and park when nothing is runnable, until the pool is quiescent. Owns the current_worker thread-local for its duration. The hot path (find_work hitting the local deque, run) touches only this worker's own lock free deque. The global lock is reached to park, for the global queue, and on every yield, a yielded fiber goes to the global tail so its peers get a turn, which is the point of yielding.

Definition at line 707 of file scheduler.c.

707 {
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);
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}
static srn_fiber_result_t waiter(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:51
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:240
bool(* srn_fiber_park_fn)(srn_fiber_t *self, void *arg)
Suspend commit callback.
Definition fiber.h:266
void srn_reactor_consume(srn_reactor_t *reactor, size_t channel)
Runs on the worker loop who owns the channel.
Definition io.c:123
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
static void registry_remove(srn_scheduler_t *sched, srn_fiber_t *fiber)
Unlink from the registry.
Definition scheduler.c:309
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
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.
Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ current_worker

_Thread_local srn_worker_t* current_worker = nullptr
static

The worker the calling os thread is running, or null when this os thread is not running the worker routine (srn_sched_run is not active on it).

This thread-local is the seam that resolves the resumer, the current fiber, and "are we in a fiber?" – all per os thread state that cannot live in the single shared scheduler.

Definition at line 257 of file scheduler.c.