Serene Runtime 1.0.0
C runtime for the Serene programming language
Loading...
Searching...
No Matches
scheduler.c File Reference
#include <stdatomic.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/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_MAX_WORKERS   256
 Upper bound on workers per run.
 
#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_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.
 
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, int 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_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.
 
srn_fiber_tsrn_fiber_current (void)
 The fiber currently running on this os thread (the bootstrap fiber if none).
 
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.
 

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:177

Definition at line 28 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 37 of file scheduler.c.

37#define SCHED_TRACE(...) \
38 do { \
39 } 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 209 of file scheduler.c.

◆ SRN_MAX_WORKERS

#define SRN_MAX_WORKERS   256

Upper bound on workers per run.

Clamps the worker count srn_sched_run accepts.

Definition at line 44 of file scheduler.c.

Typedef Documentation

◆ srn_sched_state_t

The scheduler's lifecycle as one atomic value.

RUNNING means a run is draining the queues. STOPPING tells the workers to wind down, set at natural quiescence or by srn_sched_stop. IDLE is the resting state before a run starts. Workers read it without the lock, so it is atomic.

◆ 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 119 of file scheduler.c.

Enumeration Type Documentation

◆ srn_sched_state_t

The scheduler's lifecycle as one atomic value.

RUNNING means a run is draining the queues. STOPPING tells the workers to wind down, set at natural quiescence or by srn_sched_stop. IDLE is the resting state before a run starts. Workers read it without the lock, so it is atomic.

Enumerator
SRN_SCHED_IDLE 
SRN_SCHED_RUNNING 
SRN_SCHED_STOPPING 

Definition at line 125 of file scheduler.c.

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

Function Documentation

◆ announce_work()

static 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 402 of file scheduler.c.

402 {
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}
#define SCHED_TRACE(...)
Per-operation deque and queue tracing (push, pop, steal, wake).
Definition scheduler.c:37
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
atomic_int runnable
Definition scheduler.c:176
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:64
Here is the call graph for this function:
Here is the caller graph for this function:

◆ find_work()

static 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 616 of file scheduler.c.

616 {
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}
static srn_fiber_t * local_pop(srn_worker_t *w)
Owner only.
Definition scheduler.c:455
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 srn_fiber_t * local_steal(srn_worker_t *victim)
Thief side.
Definition scheduler.c:494
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
srn_scheduler_t * sched
Definition scheduler.c:219
Here is the call graph for this function:
Here is the caller graph for this function:

◆ global_enqueue()

static 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 530 of file scheduler.c.

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

◆ global_take()

static 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 555 of file scheduler.c.

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

◆ local_pop()

static 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 455 of file scheduler.c.

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

◆ local_push()

static 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 426 of file scheduler.c.

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

◆ local_steal()

static 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 494 of file scheduler.c.

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

◆ push_ready()

static 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 576 of file scheduler.c.

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

◆ ready_fiber()

static 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 605 of file scheduler.c.

605 {
607 if (atomic_compare_exchange_strong(&fiber->state, &expected, SRN_FIBER_READY)) {
608 push_ready(sched, fiber);
609 }
610}
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:184
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:188
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:576
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:217
Here is the call graph for this function:
Here is the caller graph for this function:

◆ registry_add()

static 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 277 of file scheduler.c.

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

◆ registry_remove()

static 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 290 of file scheduler.c.

290 {
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}
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 (the bootstrap fiber if none).

Definition at line 930 of file scheduler.c.

930 {
931 return current_worker != nullptr ? current_worker->current : nullptr;
932}
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.

This is the seam an IO reactor, timer, or peer fiber uses to wake a fiber when the event it awaited occurs.

Definition at line 918 of file scheduler.c.

918 {
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}
#define srn_fiber_get_scheduler_m(fiber)
Definition fiber.h:119
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
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 898 of file scheduler.c.

898 {
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}
static srn_fiber_result_t worker(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:41
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 * park_arg
Definition fiber.h:229
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
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.

Definition at line 963 of file scheduler.c.

963 {
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}
srn_fiber_t * srn_fiber_current(void)
The fiber currently running on this os thread (the bootstrap fiber if none).
Definition scheduler.c:930
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
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:223
#define PANIC_IF(cond, msg)
Definition utils.h:57
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 934 of file scheduler.c.

934 {
936 return &current_worker->loop;
937}
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 876 of file scheduler.c.

876 {
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}
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 593 of file scheduler.c.

593 {
594 PANIC_IF_NULL(sched);
595 PANIC_IF_NULL(fiber);
596
597 fiber->state = SRN_FIBER_READY;
598 push_ready(sched, fiber);
599}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_init()

srn_scheduler_t * srn_sched_init ( srn_engine_t * engine)
nodiscard

Definition at line 246 of file scheduler.c.

246 {
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}
#define srn_mm_immortal_allocate(mm, T)
Definition interface.h:169
srn_mm_t * mm
Memory manager.
Definition engine.h:58
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_thread_t * os_threads
Definition scheduler.c:192
_Atomic srn_sched_state_t state
Definition scheduler.c:178
@ SRN_THREAD_OK
Definition thread.h:62
srn_thread_status_t srn_mutex_init(srn_mutex_t *m)
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 305 of file scheduler.c.

305 {
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}
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
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,
int 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 784 of file scheduler.c.

784 {
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}
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
#define SRN_MAX_WORKERS
Upper bound on workers per run.
Definition scheduler.c:44
static void worker_main(void *arg)
The entry an os thread starts in.
Definition scheduler.c:778
srn_fiber_t * current
Definition scheduler.c:221
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:51
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 314 of file scheduler.c.

314 {
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}
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_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
#define SCHED_LOG(FMT,...)
Definition scheduler.c:28
void srn_fiber_stack_free(srn_fiber_stack_t stack)
Definition stack_posix.c:67
srn_fiber_stack_t stack
Definition fiber.h:211
const char * name
For debugging purposes.
Definition fiber.h:270
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, including a signal-driven context or a fiber. A no-op if the scheduler is not running.

Definition at line 849 of file scheduler.c.

849 {
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}
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:

◆ wait_for_park()

static 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 945 of file scheduler.c.

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

◆ worker_main()

static 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 778 of file scheduler.c.

778 {
779 srn_worker_t *worker = arg;
782}
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
static void worker_run(srn_worker_t *worker)
Run the worker routine over worker on the calling os thread.
Definition scheduler.c:652
Here is the call graph for this function:
Here is the caller graph for this function:

◆ worker_run()

static 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, re-enqueue on yield) touches only this worker's own lock free deque. The global lock is reached only to park or for the global queue.

Definition at line 652 of file scheduler.c.

652 {
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}
static srn_fiber_result_t waiter(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:48
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:186
bool(* srn_fiber_park_fn)(srn_fiber_t *self, void *arg)
Suspend commit callback.
Definition fiber.h:206
static void registry_remove(srn_scheduler_t *sched, srn_fiber_t *fiber)
Unlink from the registry.
Definition scheduler.c:290
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
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 240 of file scheduler.c.