Add LocalWaker support · Issue #191 · rust-lang/libs-team (original) (raw)
Proposal
Problem statement
The current implementation of Waker is Send + Sync, which means that it can be freely moved between threads. While this is necessary for work stealing, it also imposes a performance penalty on single threaded and thread per core (TPC) runtimes. This runtime cost is contrary to Rust's zero cost abstraction philosophy. Furthermore, this missing API has led some async runtimes to implement unsound wakers, both by neglect and intentionally.
Motivation, use-cases
Local wakers leverage Rust's compile-time thread safety to avoid unnecessary thread synchronization, offering a compile-time guarantee that a waker has not been moved to another thread. This allows async runtimes to specialize their waker implementations and skip unnecessary atomic operations, thereby improving performance.
It is noteworthy to mention that while this is especially useful for TPC runtimes, work stealing runtimes may also benefit from performing this specialization. So this should not be considered a niche use case.
Solution sketches
Construction
Constructing a local waker is done in the same way as a shared waker. You just use the from_raw
function, which takes a RawWaker
.
use std::task::LocalWaker; let waker = unsafe { LocalWaker::from_raw(raw_waker) }
Alternatively, the LocalWake
trait may be used analogously to Wake
.
use std::task::LocalWake;
thread_local! { /// A queue containing all woken tasks static WOKEN_TASKS: RefCell<VecDeque>; }
struct Task(Box<dyn Future<Output = ()>>);
impl LocalWake for Task { fn wake(self: Rc) { WOKEN_TASKS.with(|woken_tasks| { woken_tasks.borrow_mut().push_back(self) }) } }
The safety requirements for constructing a LocalWaker
from a RawWaker
would be the same as a Waker
, except for thread safety.
Usage
A local waker can be accessed with the local_waker
method on Context
and woken just like a regular waker. All contexts will return a valid local_waker
, regardless of whether they are specialized for this case or not.
let local_waker: &LocalWaker = cx.local_waker(); local_waker.wake();
ContextBuilder
In order to construct a specialized Context
, the ConstextBuilder
type must be used. This type may be extended in the future to allow for more functionality for Context
.
use std::task::{Context, LocalWaker, Waker, ContextBuilder};
let waker: Waker = /* ... /; let local_waker: LocalWaker = / ... */;
let context = ContextBuilder::new() .waker(&waker) .local_waker(&local_waker) .build() .expect("at least one waker must be set");
Then it can be accessed with the local_waker
method on Context
.
let local_waker: &LocalWaker = cx.local_waker();
If a LocalWaker
is not set on a context, this one would still return a valid LocalWaker
, because a local waker can be trivially constructed from the context's waker.
If a runtime does not intend to support thread safe wakers, they should not provide a Waker
to ContextBuilder
, and it will construct a Context
that panics in the call to waker()
.
Links and related work
- IRLO thread about LocalWaker
- Context made !Send + !Sync with this use case in mind
- Monoio's Waker being unsound
- Glommio's Waker being unsound
- Withoutboat's comments on the waker API v1
- Withoutboat's comments on the waker API v2
What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.