|
Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
|
#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/rt/reactor.h"#include "serene/utils.h"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_t * | srn_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_t * | local_pop (srn_worker_t *w) |
| Owner only. | |
| static srn_fiber_t * | local_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_t * | global_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_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 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_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. | |
| srn_fiber_t * | srn_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. | |
| 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_t * | current_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). | |
| #define SCHED_LOG | ( | FMT, | |
| ... ) |
Definition at line 29 of file scheduler.c.
| #define SCHED_TRACE | ( | ... | ) |
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 38 of file scheduler.c.
| #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 217 of file scheduler.c.
| typedef enum srn_sched_state_t 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.
| 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:
Definition at line 116 of file scheduler.c.
| enum 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 130 of file scheduler.c.
|
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 420 of file scheduler.c.
|
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 661 of file scheduler.c.
|
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 552 of file scheduler.c.
|
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 578 of file scheduler.c.
|
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 473 of file scheduler.c.
|
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 444 of file scheduler.c.
|
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 515 of file scheduler.c.
|
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 599 of file scheduler.c.
|
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 650 of file scheduler.c.
|
static |
Insert at the head of the registry. Caller must hold sched->lock.
Definition at line 290 of file scheduler.c.
|
static |
Unlink from the registry.
O(1), thanks to the back pointer. Caller must hold sched->lock.
Definition at line 303 of file scheduler.c.
| 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 1058 of file scheduler.c.
| 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 1044 of file scheduler.c.
| 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 630 of file scheduler.c.
| 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 1024 of file scheduler.c.
| 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 1091 of file scheduler.c.
| 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 1062 of file scheduler.c.
| 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 1000 of file scheduler.c.
| 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 1108 of file scheduler.c.
| 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 1102 of file scheduler.c.
| 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 963 of file scheduler.c.
| 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 622 of file scheduler.c.
|
nodiscard |
Definition at line 257 of file scheduler.c.
| 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 318 of file scheduler.c.
| 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 843 of file scheduler.c.
| 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 327 of file scheduler.c.
| 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 939 of file scheduler.c.
| 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 1116 of file scheduler.c.
|
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 1073 of file scheduler.c.
|
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 837 of file scheduler.c.
|
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 698 of file scheduler.c.
|
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 251 of file scheduler.c.