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().

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.