feat: fibers inherit the spawn-time context (Phase 0 of Io unification)
A fiber body previously ran under the static `__sx_default_context`: the
`abi(.c)` `fib_dispatch` frame has no implicit context param, so a
`push Context { … }` around `spawn` was invisible inside the fiber. That
makes it impossible to fold a fiber scheduler behind `context.io` — a
worker's `context.io.*` would resolve to the blocking default, not the
scheduler that spawned it.
`Scheduler.spawn` now snapshots the live `context` into `Fiber.dctx`, and
`fib_dispatch` re-pushes it (`push self.dctx { self.body() }`) around the
body. So a capability installed before `spawn` (allocator, io, data) is
visible to the worker, and a worker spawned under `push Context { io = … }`
sees that `io` as `context.io`.
Behavior-preserving for fibers spawned under the default context (the
snapshot just re-pushes that same default — full suite green). Context is
parameter-threaded per fiber stack, so interleaved fibers with different
contexts don't leak across the `swap_context` (verified: two fibers with
distinct `context.data` each keep their own across a `yield_now`).
Locks: examples/concurrency/1822-concurrency-fiber-context-inherit.sx
(byte-identical aarch64-macOS host + aarch64-linux container).
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
// Stream B2/A1 — a fiber INHERITS the dynamic `context` in force when it was
|
||||
// spawned. Previously a fiber body ran under the static `__sx_default_context`
|
||||
// (the `abi(.c)` `fib_dispatch` dropped the implicit context), so a
|
||||
// `push Context { … }` around `spawn` was invisible inside the fiber. Now
|
||||
// `Scheduler.spawn` snapshots `context` into the fiber and `fib_dispatch`
|
||||
// re-pushes it around the body — so a capability installed before `spawn`
|
||||
// (here a marker in `context.data`) is visible to the worker.
|
||||
//
|
||||
// This is the foundation for folding a fiber scheduler behind `context.io`: a
|
||||
// worker's `context.io.*` must resolve to the scheduler that spawned it, not the
|
||||
// blocking default. Behavior-preserving for fibers spawned under the default
|
||||
// context (the snapshot just re-pushes that same default).
|
||||
//
|
||||
// aarch64-pinned (the scheduler's per-arch asm): runs end-to-end on a matching
|
||||
// host (macOS + linux), ir-only on a mismatch.
|
||||
#import "modules/std.sx";
|
||||
sched :: #import "modules/std/sched.sx";
|
||||
|
||||
Marker :: struct { id: i64; }
|
||||
|
||||
main :: () -> i64 {
|
||||
mk := Marker.{ id = 7 };
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s;
|
||||
print("outside: marker id = {}\n", mk.id);
|
||||
push Context.{ allocator = context.allocator, data = xx @mk, io = context.io } {
|
||||
ps.spawn(() => {
|
||||
m : *Marker = xx context.data; // inherited from the spawn-time context
|
||||
print("inside fiber: context.data marker id = {}\n", m.id);
|
||||
});
|
||||
ps.run();
|
||||
}
|
||||
print("done\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -88,6 +88,16 @@ Fiber :: struct {
|
||||
stack_len: i64; // GUARD + STACK, for munmap
|
||||
id: i64;
|
||||
next: *Fiber; // intrusive FIFO ready-queue link
|
||||
// The dynamic `context` in force when this fiber was SPAWNED, captured by
|
||||
// value. `fib_dispatch` re-pushes it around the body so the fiber runs under
|
||||
// its spawner's context — not the static `__sx_default_context` the `abi(.c)`
|
||||
// dispatch would otherwise leave in force. This is what lets a fiber-backed
|
||||
// `Io` scheduler be reached as `context.io` INSIDE a worker (the whole point
|
||||
// of folding the scheduler behind `context.io`): a worker's
|
||||
// `context.io.suspend_raw`/`ready` resolve to the scheduler that spawned it,
|
||||
// not the blocking default. Behavior-preserving for fibers spawned under the
|
||||
// default context (the capture just re-pushes that same default).
|
||||
dctx: Context;
|
||||
}
|
||||
|
||||
// A pending virtual-time timer: wake `fiber` once the virtual clock reaches
|
||||
@@ -190,6 +200,9 @@ Scheduler :: struct {
|
||||
f.sched = self;
|
||||
f.id = self.next_id;
|
||||
f.next = null;
|
||||
// Snapshot the spawn-time dynamic context (see `Fiber.dctx`).
|
||||
// `fib_dispatch` re-pushes it so the body inherits it.
|
||||
f.dctx = context;
|
||||
self.next_id = self.next_id + 1;
|
||||
self.n_spawned = self.n_spawned + 1;
|
||||
|
||||
@@ -650,7 +663,11 @@ T
|
||||
// `run()` call site. Fiber bodies do not inherit a caller-scoped allocator; a
|
||||
// body that needs one must capture it explicitly (the long-lived-container rule).
|
||||
fib_dispatch :: (self: *Fiber) abi(.c) {
|
||||
self.body();
|
||||
// Run the body under the context captured at spawn (`Fiber.dctx`), so the
|
||||
// fiber inherits its spawner's `context` — notably `context.io` resolves to a
|
||||
// fiber-backed scheduler installed as `context.io`, not the static
|
||||
// `__sx_default_context` this `abi(.c)` frame would otherwise leave in force.
|
||||
push self.dctx { self.body(); }
|
||||
self.state = .done;
|
||||
swap_context(@self.ctx, @self.sched.sched_ctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user