Concurrency runtime: deferred socket I/O
How one non-blocking socket op suspends and resumes on the single-threaded fiber
scheduler. The runtime pieces live in quilon-rt/src/: the socket types in
net.rs, the scheduler + readiness plumbing in scheduler.rs, the mio poll
wrapper in reactor.rs, and the fiber-stack GC integration in gc.rs.
The trace below follows a single TcpStream::read that has to wait for data.
sequenceDiagram participant F as Fiber participant S as Scheduler participant R as Reactor (mio) participant K as Kernel (epoll) participant G as GC (Boehm)
F->>F: TcpStream::read(buf) Note over F: io_loop: inner.read() returns WouldBlock F->>R: reregister_readiness(token, READABLE) R->>K: epoll_ctl arm interest (non-blocking syscall) F->>S: park_on_readiness(token) Note over F,S: userspace context switch, no OS wait S->>S: readiness_waiters: token to fiber id S->>S: run other ready fibers
opt A GC runs while this fiber is parked G->>G: stop the world G->>G: push_fiber_roots (installed as GC_push_other_roots) G->>F: GC_push_all_eager(low, high) over the parked stack Note over G,F: parked fiber's live objects survive end
Note over S: ready queue empty S->>R: Reactor::wait(nearest sleep deadline) R->>K: epoll_wait(timeout) Note over R,K: the ONE OS-level block, covers ALL parked fibers K-->>R: token is readable R-->>S: ready_tokens() yields token S->>S: wait_and_wake: readiness_waiters.remove(token), ready.push_back(id) S->>F: resume Note over F: io_loop retries inner.read() F-->>F: Ok(n), read succeedsWalkthrough
Section titled “Walkthrough”- Parking is a userspace context switch, not an OS wait. When
inner.readreturnsWouldBlock,io_looparms read interest withreregister_readiness(a cheap, non-blockingepoll_ctl) and then callspark_on_readiness, which suspends the fiber back to the scheduler via acorosenseistack switch. The scheduler recordsreadiness_waiters[token] = fiber idand keeps running other ready fibers. No thread blocks here. - The only OS-level block is one
epoll_wait. Once the ready queue drains, the scheduler callsReactor::waitwith the nearest sleep deadline as the timeout; that is the singleepoll_waitthat covers every parked fiber at once. Whichever fires first — a socket token becoming ready or the timer elapsing — returns it.ready_tokens()then hands each fired token towait_and_wake, which maps it back throughreadiness_waitersto the exact fiber and requeues it; on resume the op simply retries. - Why fiber stacks are registered with the GC. Boehm only scans the OS
thread’s stack, but a parked fiber’s live roots sit on its own
corosenseistack. Each fiber’s stack range is registered (gc::register), so on a collectionpush_fiber_rootspushes every parked fiber’s range withGC_push_all_eager, while the running fiber is covered byGC_set_stackbottom— so a collection triggered by another fiber’s allocation never frees a socket-parked fiber’s live objects.