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<Task>>;
}
struct Task(Box<dyn Future<Output = ()>>);
impl LocalWake for Task {
fn wake(self: Rc<Self>) {
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
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.
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_rawfunction, which takes aRawWaker.Alternatively, the
LocalWaketrait may be used analogously toWake.The safety requirements for constructing a
LocalWakerfrom aRawWakerwould be the same as aWaker, except for thread safety.Usage
A local waker can be accessed with the
local_wakermethod onContextand woken just like a regular waker. All contexts will return a validlocal_waker, regardless of whether they are specialized for this case or not.ContextBuilder
In order to construct a specialized
Context, theConstextBuildertype must be used. This type may be extended in the future to allow for more functionality forContext.Then it can be accessed with the
local_wakermethod onContext.If a
LocalWakeris not set on a context, this one would still return a validLocalWaker, 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
WakertoContextBuilder, and it will construct aContextthat panics in the call towaker().Links and related work
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.