Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
fiber.h File Reference

AI Generated (🤦) Fiber subsystem overview. More...

#include <stddef.h>
#include <stdint.h>
#include "serene/rt/trace.h"
Include dependency graph for fiber.h:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Data Structures

struct  srn_fiber_ctx_t
 The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from. More...
struct  srn_fiber_stack_t
 One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour. More...
struct  srn_fiber_t

Macros

#define SRN_ASAN   0
#define SRN_TSAN   0
#define FIBER_TRACEPOINT(...)
#define srn_fiber_get_scheduler_m(fiber)
#define SRN_FIBER_DEFAULT_STACK_SIZE   (128U * 1024U)
 Default size of every fiber stack.
#define SRN_FIBER_NAME_MAX   32U
 Size of the embedded debug name buffer on every fiber, terminator included.
#define SRN_FIBER_SPAWN_COPY(ctx, entry, value)
 srn_fiber_spawn_copy for an lvalue, the address and size are taken for the caller.

Typedefs

typedef struct srn_fiber_t srn_fiber_t
typedef size_t srn_worker_id_t
typedef void * srn_fiber_result_t
 What a fiber's entry produces, type-erased.
typedef struct srn_fiber_ctx_t srn_fiber_ctx_t
 The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from.
typedef struct srn_fiber_stack_t srn_fiber_stack_t
 One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour.
typedef enum srn_fiber_state_e srn_fiber_state_t
typedef srn_fiber_result_t(* srn_fiber_entry_t) (srn_context_t *ctx, void *arg)
 The function a fiber runs.
typedef bool(* srn_fiber_park_fn) (srn_fiber_t *self, void *arg)
 Suspend commit callback.

Enumerations

enum  srn_fiber_state_e : uint8_t {
  SRN_FIBER_NEW = 0 , SRN_FIBER_READY , SRN_FIBER_RUNNING , SRN_FIBER_SUSPENDED ,
  SRN_FIBER_DONE
}

Functions

srn_scheduler_tsrn_sched_init (srn_engine_t *engine)
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.
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_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_enqueue (srn_scheduler_t *sched, srn_fiber_t *fiber)
 Place a fiber on a scheduler's ready queue, making it eligible to run.
srn_fiber_tsrn_fiber_make (srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size)
 Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
void srn_fiber_autoname (srn_engine_t *engine, char *dst, size_t size)
 Write the autogenerated debug name for a new fiber into dst.
void srn_fiber_schedule (srn_fiber_t *fiber)
 Schedule a NEW fiber, making it eligible to run.
srn_fiber_tsrn_fiber_spawn (srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
 Make and schedule a fiber with every default, the engine's scheduler, the configured stack size, and an autogenerated name.
srn_fiber_tsrn_fiber_spawn_copy (srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size)
 srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the caller's stack frame can die freely.
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)
 Park the running fiber until a party calls 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.
srn_fiber_result_t srn_fiber_wait_for (srn_fiber_t *target)
 Block the calling fiber until target finishes, then return its result.
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.
void srn_fiber_swap (srn_fiber_ctx_t *from, srn_fiber_ctx_t *to)
 Save the current execution context into from, restore to, and resume on to's stack.
void srn_fiber_ctx_make (srn_fiber_ctx_t *fiber_ctx, srn_fiber_stack_t stack, void(*fn)(void *), void *arg)
 Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stack.
void srn_fiber_switch (srn_fiber_t *from, srn_fiber_t *to)
 Switch from from to to, both live fibers, telling AddressSanitizer about the stack change.
void srn_fiber_switch_final (srn_fiber_t *to)
 Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers to to and never comes back.
void srn_fiber_on_entry (srn_fiber_t *from)
 Call as the first action inside a fresh fiber's entry.
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.
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 away from it and back.
srn_fiber_stack_t srn_fiber_stack_alloc (size_t size)
 Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE when size is 0.
void srn_fiber_stack_free (srn_fiber_stack_t stack)
static size_t srn_fiber_stack_size (srn_fiber_stack_t s)
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.
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.

Detailed Description

AI Generated (🤦) Fiber subsystem overview.

Stackful, cooperative fibers for the runtime. A fiber is a function plus its own stack, so it can pause at any call depth and resume later. Nothing is preempted. A fiber runs until it explicitly yields, suspends, or finishes.

Terminology (used throughout the fiber subsystem) fiber a unit of work with its own stack. It can pause and resume. os thread a thread the operating system schedules. worker the state one os thread uses to run fibers. It holds a local queue of ready fibers and a worker's loop. There is one worker per os thread. worker's loop a special fiber that stands in for the os thread. A running fiber switches back to it to hand control over, and it picks the next fiber to run. worker routine the steps an os thread repeats. It takes a fiber, switches into it, deals with how the fiber gave up control, and parks when no fiber is runnable. scheduler there is exactly one. It owns the shared ready queue and the list of live fibers and decides what runs next. It does not run fibers itself, workers do. suspend a fiber steps off every queue and waits to be woken. Its os thread stays free to run other fibers. park an os thread blocks because no fiber is runnable anywhere. A notify wakes it.

Every fiber stack is the same fixed size (SRN_FIBER_DEFAULT_STACK_SIZE), one mapping with a guard page that never grows. Uniform size is a deliberate constraint, not an accident. It lets a finished fiber's stack be handed to any later fiber (the stack ring TODO below) without tracking size classes, and it keeps switch and mapping costs predictable.

Control flow is a hub. A fiber never jumps to another fiber. It switches back to its worker's loop, which picks the next one.

scheduler ready queue:  [F2] -> [F5] -> ...
                           |  the os thread takes the head
                           v
   +----------->  worker's loop  --- switch --->  running fiber
   |                                                  |
   |  yield    (re-enqueue, run next) ----------------+
   |  suspend  (off-queue until srn_fiber_ready) -----+
   |  finish   (reap) --------------------------------+
   +--------------------------------------------------+

A running fiber gives up control three ways:

  • yield still runnable, so it moves to the back of the ready queue.
  • suspend off every queue until some party calls srn_fiber_ready. Fibers, workers, and the reactor may wake it; the reactor is the only waker outside the worker pool (see srn_fiber_ready).
  • finish its entry returns and the fiber is reaped.

Signal handling is not part of this subsystem. No scheduler entry point is async signal safe, and by design: a runtime that owns an event loop folds signals into it rather than promising handler safety call by call. srn_sched_stop and srn_sched_drain take the scheduler mutex and signal its condition variable, so calling them from a signal handler can self-deadlock. A signal-initiated shutdown routes the signal into ordinary thread context first (a dedicated thread in sigwait, or a signalfd registered with the reactor) and calls stop or drain from there.

The spec book's fibers chapter (docs/spec/fibers.typ) is the long-form reference. Keep this overview in step with it.

Definition in file fiber.h.

Macro Definition Documentation

◆ FIBER_TRACEPOINT

#define FIBER_TRACEPOINT ( ...)
Value:
SRN_TRACEPOINT_WITH_GROUP(fiber __VA_OPT__(, ) __VA_ARGS__)
#define SRN_TRACEPOINT_WITH_GROUP(group, name,...)
Definition trace.h:80

Definition at line 146 of file fiber.h.

◆ SRN_ASAN

#define SRN_ASAN   0

Definition at line 132 of file fiber.h.

◆ SRN_FIBER_DEFAULT_STACK_SIZE

#define SRN_FIBER_DEFAULT_STACK_SIZE   (128U * 1024U)

Default size of every fiber stack.

All fiber stacks share one size (see the subsystem overview), so this is a process-wide value, not a per-fiber one: the default here, eventually overridable through a CLI argument.

A fiber stack is fixed and cannot grow (see the fibers chapter), so the size must hold realistic call chains yet stay cheap to reserve in bulk. 128 KiB strikes that balance, deep enough for the runtime's C and JIT frames, while lazy page commit means a shallow fiber faults in only the one or two pages it touches and leaves the rest a virtual reservation. For scale, an OS thread stack is far too large to spawn fibers by the thousand, and a few KiB would overflow on modest nesting.

Definition at line 184 of file fiber.h.

◆ srn_fiber_get_scheduler_m

#define srn_fiber_get_scheduler_m ( fiber)
Value:
fiber->ctx->engine->scheduler

Definition at line 163 of file fiber.h.

◆ SRN_FIBER_NAME_MAX

#define SRN_FIBER_NAME_MAX   32U

Size of the embedded debug name buffer on every fiber, terminator included.

Large enough for the autogenerated fiber_<id> spelling of any 64 bit id.

Definition at line 191 of file fiber.h.

◆ SRN_FIBER_SPAWN_COPY

#define SRN_FIBER_SPAWN_COPY ( ctx,
entry,
value )
Value:
srn_fiber_spawn_copy(ctx, entry, &(value), sizeof(value))
srn_fiber_t * srn_fiber_spawn_copy(srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size)
srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the...
Definition fiber.c:253

srn_fiber_spawn_copy for an lvalue, the address and size are taken for the caller.

Definition at line 474 of file fiber.h.

474#define SRN_FIBER_SPAWN_COPY(ctx, entry, value) \
475 srn_fiber_spawn_copy(ctx, entry, &(value), sizeof(value))

◆ SRN_TSAN

#define SRN_TSAN   0

Definition at line 143 of file fiber.h.

Typedef Documentation

◆ srn_fiber_ctx_t

typedef struct srn_fiber_ctx_t srn_fiber_ctx_t

The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from.

The callee-saved registers live on the fiber's own stack, pushed by srn_fiber_swap and popped when it is resumed.

◆ srn_fiber_entry_t

typedef srn_fiber_result_t(* srn_fiber_entry_t) (srn_context_t *ctx, void *arg)

The function a fiber runs.

Allocations made through ctx land in the context's block. TODO(lxsameer): The shape is generic until code generation produces fibers, at which point it adopts the Serene ABI. TODO(lxsameer): Should we support two types of entry functions? one for C calling convention and one for FastC?

Definition at line 255 of file fiber.h.

◆ srn_fiber_park_fn

typedef bool(* srn_fiber_park_fn) (srn_fiber_t *self, void *arg)

Suspend commit callback.

The worker routine runs it on its own side once the fiber has switched out. It re-checks the awaited condition and only then registers self with whatever will wake it, returning true to stay suspended, or false to resume self at once because the condition already holds. The order matters, registering before the re-check can leave a live registration behind a false return, and that stale waker would wake the fiber's NEXT suspend. It must not switch fibers.

Definition at line 266 of file fiber.h.

◆ srn_fiber_result_t

typedef void* srn_fiber_result_t

What a fiber's entry produces, type-erased.

It is an alias for void *, so the fiber subsystem carries no dependency on the value model. A caller that runs fibers over Serene values casts to and from its own type at the boundary.

Definition at line 161 of file fiber.h.

◆ srn_fiber_stack_t

typedef struct srn_fiber_stack_t srn_fiber_stack_t

One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour.

With addresses increasing to the right, the layout is:

|... guard page ...|.......... frames ..........| ^ guard ^ limit ^ start

guard is the mmap base, the lowest page of the region. The stack grows downward, so frames start at the high end (start) and grow toward limit; one step past limit lands on the protected guard page and faults.

◆ srn_fiber_state_t

◆ srn_fiber_t

typedef struct srn_fiber_t srn_fiber_t

Definition at line 150 of file fiber.h.

◆ srn_worker_id_t

typedef size_t srn_worker_id_t

Definition at line 153 of file fiber.h.

Enumeration Type Documentation

◆ srn_fiber_state_e

enum srn_fiber_state_e : uint8_t
Enumerator
SRN_FIBER_NEW 

Created, stack mapped, never resumed.

SRN_FIBER_READY 

On the run queue, eligible to run.

SRN_FIBER_RUNNING 

Currently executing.

SRN_FIBER_SUSPENDED 

Parked off the run queue, awaits srn_fiber_ready.

SRN_FIBER_DONE 

Entry returned. The result is final.

Definition at line 234 of file fiber.h.

234 : uint8_t {
235 /// Created, stack mapped, never resumed
236 SRN_FIBER_NEW = 0,
237 /// On the run queue, eligible to run
239 /// Currently executing
241 /// Parked off the run queue, awaits srn_fiber_ready
243 /// Entry returned. The result is final
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:236
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:240
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:238
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:244
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:242
enum srn_fiber_state_e srn_fiber_state_t

Function Documentation

◆ 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
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
The state one os thread uses to run fibers.
Definition scheduler.c:230
uint64_t spawned
Count of fibers this worker has created so far.
Definition scheduler.c:238
srn_worker_id_t id
Definition scheduler.c:234
#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:

◆ srn_fiber_ctx_make()

void srn_fiber_ctx_make ( srn_fiber_ctx_t * fiber_ctx,
srn_fiber_stack_t stack,
void(* fn )(void *),
void * arg )

Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stack.

fn is the fiber's entry, a void (*)(void *) that runs on the new stack and receives arg. It must NEVER return – there is no frame beneath it, so a return traps in the trampoline. Instead it transfers control away with srn_fiber_switch (to yield) or srn_fiber_switch_final (when finished), and as its first action it calls srn_fiber_on_entry(). Typical shape:

static void worker(void *arg) { srn_fiber_on_entry(&scheduler); // hand the switch to the sanitizer ... do the fiber's work using arg ... srn_fiber_switch_final(&scheduler); // finished, never returns }

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_init_thread()

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 away from it and back.

The thread's stack bounds are not queried here (there is no portable way). Under the sanitizer they are recorded by the first fiber's srn_fiber_on_entry. The saved context is written by the first switch away.

Definition at line 153 of file fiber.c.

153 {
154 // Represent the calling thread as the running fiber. The saved context
155 // (fiber_ctx) stays empty, and the first switch away from the thread fills
156 // it.
157 memset(f, 0, sizeof(*f));
159 (void)snprintf(f->name, sizeof(f->name), "thread");
160#if SRN_ASAN
161 // ASan needs this loop's stack bounds on every switch back to it, and a
162 // worker that only ever resumes stolen fibers never launches a fresh
163 // fiber, so srn_fiber_on_entry would never report them. Run a throwaway
164 // fiber once, its entry records the bounds through the same protocol
165 // every real fiber uses. The probe finishes before the switch back, so
166 // srn_fiber_switch_final already has real bounds to announce.
167 srn_fiber_t probe;
168 memset(&probe, 0, sizeof(probe));
169 probe.state = SRN_FIBER_READY;
170 (void)snprintf(probe.name, sizeof(probe.name), "stack-bounds-probe");
171 probe.stack = srn_fiber_stack_alloc(1); // rounds up to one page plus guard
172 srn_fiber_ctx_make(&probe.fiber_ctx, probe.stack, stack_bounds_probe, f);
173 srn_fiber_switch(f, &probe);
175#endif
176#if SRN_TSAN
177 // The loop fiber stands in for this OS thread, so it takes the thread's own
178 // current TSan fiber rather than a freshly created one.
179 f->tsan_fiber = __tsan_get_current_fiber();
180#endif
181}
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
srn_fiber_stack_t srn_fiber_stack_alloc(size_t size)
Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE whe...
void srn_fiber_ctx_make(srn_fiber_ctx_t *fiber_ctx, srn_fiber_stack_t stack, void(*fn)(void *), void *arg)
Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stac...
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
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:277
srn_fiber_stack_t stack
Definition fiber.h:271
srn_fiber_ctx_t fiber_ctx
Saved stack pointer (see srn_fiber_ctx_t).
Definition fiber.h:270
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_make()

srn_fiber_t * srn_fiber_make ( srn_context_t * ctx,
srn_scheduler_t * sched,
const char * name,
srn_fiber_entry_t entry,
void * arg,
size_t stack_size )
nodiscard

Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.

The fiber stays NEW until srn_fiber_schedule starts it, so a caller can build several fibers and wire them up before any of them runs. Registration at creation means the scheduler reaps every fiber at shutdown, scheduled or not.

name is a debug label copied into the fiber, truncated to SRN_FIBER_NAME_MAX - 1 bytes, or nullptr for an autogenerated unique one. A stack_size of 0 selects the configured per fiber default.

Definition at line 200 of file fiber.c.

203 {
204
205 srn_fiber_t *f = ALLOC(ctx, srn_fiber_t);
206 PANIC_IF_NULL(f);
207 memset((void *)f, 0, sizeof(srn_fiber_t));
208
209 // TODO(lxsameer): Make the fiber stack configurable via cli arg or something.
210 // This is the acquire side of the stack-ring TODO in fiber.h: a pooled stack
211 // would be pulled from the per-thread ring here, falling back to a fresh
212 // mapping only on a miss.
213 // A zero `stack_size` falls back to the configured per fiber default.
214 f->stack =
215 srn_fiber_stack_alloc(stack_size != 0 ? stack_size : ctx->engine->config.fiber.stack_size);
216 f->ctx = ctx;
217 f->state = SRN_FIBER_NEW;
218 f->entry = entry;
219 f->arg = arg;
220
221 if (name != nullptr) {
222 (void)snprintf(f->name, sizeof(f->name), "%s", name);
223 } else {
224 srn_fiber_autoname(ctx->engine, f->name, sizeof(f->name));
225 }
226
227 FIBER_TRACEPOINT(fiber_created, f->name, srn_fiber_stack_size(f->stack));
228
229#if SRN_TSAN
230 f->tsan_fiber = __tsan_create_fiber(0);
231#endif
232 srn_fiber_ctx_make(&f->fiber_ctx, f->stack, &srn_fiber_launcher, (void *)f);
233
234 // Register before enqueuing, the scheduler must know about the fiber for its
235 // whole life, independent of which queue (if any) it currently sits on. The
236 // registry is how a SUSPENDED fiber, off every run queue, stays reachable for
237 // cleanup and cancellation.
238 srn_sched_register(sched, f);
239
240 // The fiber stays NEW. srn_fiber_schedule makes it runnable, so a caller
241 // can build several fibers and wire them up before any of them runs.
242 return f;
243}
#define ALLOC(ctx, T)
Definition context.h:84
static void srn_fiber_launcher(void *fiber_ptr)
Definition fiber.c:183
#define FIBER_TRACEPOINT(...)
Definition fiber.h:146
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:625
void srn_sched_register(srn_scheduler_t *sched, srn_fiber_t *fiber)
Record a fiber in the scheduler's registry of live fibers, where it stays until it is reaped.
Definition scheduler.c:324
void srn_fiber_autoname(srn_engine_t *engine, char *dst, size_t size)
Write the autogenerated debug name for a new fiber into dst.
Definition scheduler.c:1141
srn_fiber_config_t fiber
srn_engine_t * engine
Long term state of the compiler.
Definition context.h:49
srn_configuration_t config
The runtime's tunable knobs, the single source for every configurable value (see configuration....
Definition engine.h:62
size_t stack_size
Size of every fiber stack, in bytes.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_on_entry()

void srn_fiber_on_entry ( srn_fiber_t * from)

Call as the first action inside a fresh fiber's entry.

from is the fiber that started this one (the scheduler, or the bootstrap thread fiber). It completes the switch for AddressSanitizer and, since from's stack bounds only become discoverable here, records them into from so a later switch back is tracked. from may be null. A no-op when the sanitizer is not in use.

Definition at line 110 of file fiber.c.

110 {
111#if SRN_ASAN
112 // A nullptr fake-stack, a fresh fiber has no previously saved bookkeeping.
113 // The out-params report the stack this fiber was started from -- this is the
114 // one chance to learn `from`'s bounds (there is no portable way to query a
115 // thread's own stack), and a later switch back to `from` needs them, so
116 // record them. They flow through the sanitizer rather than libc.
117 const void *bottom = nullptr;
118 size_t size = 0;
119 __sanitizer_finish_switch_fiber(nullptr, &bottom, &size);
120
121 if (from != nullptr) {
122 // ASan reports the came-from stack as (bottom, size). The struct keeps the
123 // low and high boundaries, so limit = bottom and start = bottom + size.
124 from->stack.limit = (void *)bottom;
125 from->stack.start = (char *)bottom + size;
126 }
127#else
128 UNUSED(from);
129#endif
130}
void * limit
Low end of usable region.
Definition fiber.h:226
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:224
#define UNUSED(x)
Definition utils.h:45
Here is the caller graph for this function:

◆ srn_fiber_on_reap()

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.

Releases the fiber's ThreadSanitizer handle. A no-op when the sanitizer is not in use.

Definition at line 132 of file fiber.c.

132 {
133#if SRN_TSAN
134 __tsan_destroy_fiber(fiber->tsan_fiber);
135#else
136 UNUSED(fiber);
137#endif
138}
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}
static void push_ready(srn_scheduler_t *sched, srn_fiber_t *fiber)
Put a runnable fiber on a queue, with its state already set to READY.
Definition scheduler.c:606
#define 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_spawn()

srn_fiber_t * srn_fiber_spawn ( srn_context_t * ctx,
srn_fiber_entry_t entry,
void * arg )

Make and schedule a fiber with every default, the engine's scheduler, the configured stack size, and an autogenerated name.

The caller keeps ownership of arg and must keep it alive until the fiber is done with it, srn_fiber_spawn_copy lifts that burden.

Definition at line 245 of file fiber.c.

245 {
246 PANIC_IF_NULL(ctx);
247 srn_fiber_t *f = srn_fiber_make(ctx, ctx->engine->scheduler, nullptr, entry, arg, 0);
249 return f;
250}
srn_fiber_t * srn_fiber_make(srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size)
Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
Definition fiber.c:200
void srn_fiber_schedule(srn_fiber_t *fiber)
Schedule a NEW fiber, making it eligible to run.
Definition scheduler.c:638
srn_scheduler_t * scheduler
The fiber scheduler, that is the entry point of the fiber subsystem.
Definition engine.h:75
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_spawn_copy()

srn_fiber_t * srn_fiber_spawn_copy ( srn_context_t * ctx,
srn_fiber_entry_t entry,
const void * arg,
size_t size )

srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the caller's stack frame can die freely.

The copy lives until the context is released, like the fiber itself.

Definition at line 253 of file fiber.c.

253 {
254 PANIC_IF_NULL(ctx);
255 PANIC_IF_NULL(arg);
256 // max_align_t suits any argument type, the caller only hands over bytes.
257 void *copy = srn_allocate(ctx, size, alignof(max_align_t));
258 PANIC_IF_NULL(copy);
259 memcpy(copy, arg, size);
260 return srn_fiber_spawn(ctx, entry, copy);
261}
void * srn_allocate(const srn_context_t *ctx, size_t size, size_t alignment)
Definition context.c:73
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:245
Here is the call graph for this function:

◆ srn_fiber_stack_alloc()

srn_fiber_stack_t srn_fiber_stack_alloc ( size_t size)
nodiscard

Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE when size is 0.

Platform specific.

Here is the caller graph for this function:

◆ srn_fiber_stack_free()

void srn_fiber_stack_free ( srn_fiber_stack_t stack)
Here is the caller graph for this function:

◆ srn_fiber_stack_size()

size_t srn_fiber_stack_size ( srn_fiber_stack_t s)
inlinestatic

Definition at line 625 of file fiber.h.

625 {
626 return (size_t)((char *)s.start - (char *)s.limit);
627}
Here is the caller graph for this function:

◆ srn_fiber_suspend()

void srn_fiber_suspend ( srn_fiber_park_fn commit,
void * arg )

Park the running fiber until a party calls srn_fiber_ready.

The fiber switches out first. Only then does the scheduler run commit (see srn_fiber_park_fn) to register it and confirm it should stay parked. Registering only after the park completes is what makes it race free – a waker can never observe a half parked fiber. Acts on the fiber currently running on this thread.

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 * 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_swap()

void srn_fiber_swap ( srn_fiber_ctx_t * from,
srn_fiber_ctx_t * to )

Save the current execution context into from, restore to, and resume on to's stack.

Returns, on from's stack, when a fiber later switches back into from.

Here is the caller graph for this function:

◆ srn_fiber_switch()

void srn_fiber_switch ( srn_fiber_t * from,
srn_fiber_t * to )

Switch from from to to, both live fibers, telling AddressSanitizer about the stack change.

from resumes where it left off when something later switches back into it.

Switch from from to to, both live fibers, telling AddressSanitizer about the stack change.

Not instrumented by either sanitizer, the stack swaps mid-function, which confuses ASan's fake stack and TSan's shadow stack. The explicit annotations keep each sanitizer's fiber tracking correct across the swap instead.

Definition at line 65 of file fiber.c.

65 {
66
67#if SRN_ASAN
68 const size_t size = srn_fiber_stack_size(to->stack);
69 __sanitizer_start_switch_fiber(&from->fake_stack, to->stack.limit, size);
70#endif
71#if SRN_TSAN
72 // Move TSan's notion of the running fiber to `to` before the stack swaps. A
73 // fiber built outside srn_fiber_make / srn_fiber_init_thread has no handle
74 // and is simply not tracked. Only the low-level switch tests build such a
75 // fiber. Every fiber the scheduler runs has one.
76 if (to->tsan_fiber != nullptr) {
77 __tsan_switch_to_fiber(to->tsan_fiber, 0);
78 }
79#endif
81#if SRN_ASAN
82 __sanitizer_finish_switch_fiber(from->fake_stack, nullptr, nullptr);
83#endif
84}
void srn_fiber_swap(srn_fiber_ctx_t *from, srn_fiber_ctx_t *to)
Save the current execution context into from, restore to, and resume on to's stack.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_switch_final()

void srn_fiber_switch_final ( srn_fiber_t * to)

Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers to to and never comes back.

The fiber's entry function therefore ends at this call – any code after it is unreachable, which is why this is [[noreturn]]. Nothing is freed here. The spent fiber's stack is left frozen and reclaimed later by its owner (the scheduler reaps a finished fiber and frees its stack via srn_fiber_stack_free).

Definition at line 87 of file fiber.c.

87 {
88#if SRN_ASAN
89 // A nullptr fake-stack tells ASan the current fiber is finished and will not
90 // resume, so it can discard the bookkeeping rather than leak it.
91 const size_t size = srn_fiber_stack_size(to->stack);
92 __sanitizer_start_switch_fiber(nullptr, to->stack.limit, size);
93#endif
94#if SRN_TSAN
95 // The finished fiber's handle is released later, at reap. Here just move
96 // TSan to `to` before the swap, when `to` has a handle (see
97 // srn_fiber_switch).
98 if (to->tsan_fiber != nullptr) {
99 __tsan_switch_to_fiber(to->tsan_fiber, 0);
100 }
101#endif
102 // srn_fiber_swap always writes the outgoing sp somewhere. This fiber is done,
103 // so discard it. Control loads `to` and never comes back.
104 srn_fiber_ctx_t discard;
105 srn_fiber_swap(&discard, &to->fiber_ctx);
107}
The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switc...
Definition fiber.h:202
#define SHOULD_NOT_HAPPEN
Definition utils.h:83
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}
@ SRN_SCHED_RUNNING
Definition scheduler.c:134
_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_sched_state_t
The scheduler's lifecycle as one atomic value.
Definition scheduler.c:132
@ SRN_SCHED_DRAINING
Definition scheduler.c:135
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_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_SCHED_IDLE
Definition scheduler.c:133
srn_mm_t * mm
Memory manager.
Definition engine.h:65
atomic_size_t runnable
Definition scheduler.c:185
bool destroyed
Set once srn_sched_shutdown has torn the scheduler down.
Definition scheduler.c:211
srn_engine_t * engine
Definition scheduler.c:140
_Atomic bool run_active
True for the duration of an srn_sched_run call.
Definition scheduler.c:207
srn_fiber_t * registry
Registry, head of the doubly-linked list (through reg_prev/reg_next) of every live fiber,...
Definition scheduler.c:158
atomic_size_t idle
Definition scheduler.c:184
srn_fiber_t * ready_head
Global / overflow queue.
Definition scheduler.c:151
srn_thread_t * os_threads
Definition scheduler.c:202
srn_fiber_t * ready_tail
Definition scheduler.c:152
srn_worker_t * workers
srn_sched_run allocates these two arrays and srn_sched_shutdown frees them.
Definition scheduler.c:201
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_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.
atomic_intptr_t top
Chase-Lev deque.
Definition scheduler.c:245
srn_fiber_t * current
Definition scheduler.c:233
srn_scheduler_t * sched
Definition scheduler.c:231
atomic_intptr_t bottom
Definition scheduler.c:246
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
srn_fiber_t * reg_prev
Registry links.
Definition fiber.h:326
srn_fiber_t * reg_next
Definition fiber.h:327
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}
@ SRN_SCHED_STOPPING
Definition scheduler.c:136
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}
Here is the call graph for this function:
Here is the caller graph for this function: