11# socketry-concurrent
22
3- Stackful fibers and cooperative scheduling for Socketry's Rust packages. Use
3+ Futures with nested stackful waits for Socketry's Rust packages. Use
44the ` socketry ` package for the one-stop public entry point, or depend on this
55package directly to use the concurrency implementation on its own.
66
7- The API starts with three pieces :
7+ The library provides three building blocks :
88
9- - Stack reserves memory with inaccessible guard pages at both ends.
10- - Fiber owns a stack and switches between its caller and a closure .
11- - Pool keeps multiple stacks available for reuse .
9+ - ` Stack ` reserves memory with inaccessible guard pages at both ends.
10+ - ` Pool ` keeps multiple stacks available for reuse .
11+ - ` Scheduler ` accepts futures and polls each task on its own coroutine stack .
1212
13- Scheduler is an optional, single-threaded executor. It can run Rust futures and
14- also exposes block_current and task unblock operations for stackful code.
15- Future wakers may run on any thread; the fiber itself always resumes on the
16- thread that owns its stack. Arbitrary Rust locals on a suspended stack are not
17- tracked by the type system, so moving a suspended fiber between workers is not
18- safe. The scheduler does not migrate fibers.
13+ ` wait(future) ` lets an ordinary function wait for an asynchronous result. When
14+ the future is pending, the task's stack is suspended and the executor can run
15+ other tasks. The function returns the future's output after it completes. Calls
16+ can be nested, including inside another future's ` poll ` , without making the
17+ calling functions async.
18+
19+ ` Task::current ` provides a thread-affine reference for blocking and task-to-task
20+ transfer. ` Scheduler::spawn ` returns a separate, thread-safe ` TaskHandle ` for
21+ waking the task. Future wakers may run on any thread; each task always resumes
22+ on the thread that owns its stack.
1923
2024## Example
2125
22- use socketry_concurrent::Scheduler;
26+ use socketry_concurrent::{Scheduler, wait};
27+ use std::future::poll_fn;
28+ use std::task::Poll;
29+
30+ fn answer() -> usize {
31+ let mut first_poll = true;
32+ wait(poll_fn(|context| {
33+ if first_poll {
34+ first_poll = false;
35+ context.waker().wake_by_ref();
36+ Poll::Pending
37+ } else {
38+ Poll::Ready(42)
39+ }
40+ }))
41+ }
2342
2443 fn main() -> std::io::Result<()> {
2544 let mut scheduler = Scheduler::new(256 * 1024);
2645 scheduler.spawn(async {
27- // Await ordinary Rust futures here.
46+ assert_eq!(answer(), 42);
2847 })?;
2948 scheduler.run();
3049 Ok(())
3150 }
3251
33- Fiber::yield_now returns control to the caller. Resuming that fiber continues
34- after the yield. Dropping a suspended fiber resumes it with a private
35- cancellation panic so Rust unwinds the stack and runs local destructors.
52+ ` Scheduler::current().unwrap().wait(future) ` is also available. Both forms
53+ require a current scheduler task. A future can borrow local data and does not
54+ need to implement Send or Unpin. Each wait pins the future on the task stack
55+ and reuses the task's wake signal, without allocating a separate future or
56+ waker. A future from another runtime still needs that runtime's I/O, timer,
57+ or other services to be running.
58+
59+ Run the producer/consumer example with:
60+
61+ cargo run --package socketry-concurrent --example nested_wait
62+
63+ Use ` Task::current().unwrap().block() ` or ` Scheduler::block_current() ` to park a
64+ task. Another thread can make it runnable through its ` TaskHandle ` . Dropping a
65+ scheduler unwinds suspended task stacks and runs their local destructors.
66+
67+ ## Polling and nested waits
68+
69+ An ordinary ` .await ` can return ` Poll::Pending ` from the task's future. A nested
70+ ` wait ` , however, can suspend while that same ` poll ` call is still executing.
71+ The scheduler resumes the saved stack at that wait; it does not call the outer
72+ future's ` poll ` again until the previous invocation has returned. Wakeups for
73+ outer futures are preserved while inner waits run.
3674
37- Use Scheduler::spawn_fiber for synchronous stackful tasks. Such a task can call
38- Scheduler::block_current and be made runnable again through its TaskHandle.
75+ This implementation uses one scheduler thread for both cases. A future
76+ executor could move a Send future between completed polls, but a stack
77+ suspended inside a poll must remain on its original worker. The current
78+ scheduler implements neither work stealing nor cross-thread migration.
3979
4080## Native context switches
4181
@@ -53,14 +93,14 @@ vendored tree for reference.
5393
5494The x86-64 Linux build includes CRuby's CET shadow-stack switch path. It checks
5595whether shadow stacks are enabled at runtime and allocates a shadow stack for
56- each fiber only when needed.
96+ each task only when needed.
5797
5898## Sanitizers
5999
60100The ` address-sanitizer ` and ` thread-sanitizer ` Cargo features enable the
61101compiler runtime's fiber-switch hooks. Pair one feature with the matching Rust
62102sanitizer flag on nightly; the hooks let each runtime follow the custom stacks
63- used by Fiber .
103+ used by scheduled tasks .
64104
65105 RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target aarch64-apple-darwin --package socketry-concurrent --features address-sanitizer
66106
@@ -78,10 +118,10 @@ embedded header.
78118
79119## Current boundaries
80120
81- - Fiber and Scheduler are thread-affine and are not Send.
121+ - Task contexts and Scheduler are thread-affine and are not Send.
82122- Scheduler tasks may contain non-Send futures because they are polled only on
83123 the scheduler's owning thread.
84124- TaskHandle::unblock and future wakers are thread-safe and only enqueue work;
85- they never resume a fiber on the waking thread.
125+ they never resume a task on the waking thread.
86126- This first version does not implement work stealing or cross-thread stack
87127 migration.
0 commit comments