Waker and Context
Waker and Context
Level 10 — Rust
std::task::Wakerandstd::task::Context— the core signalling mechanism async tasks use to notify an executor that a pausedFutureis ready to be polled again.
1. Prerequisites
FutureTrait — The Future trait.
2. Term Category
Rust Asynchronous Primitives (task wake notification & context): std::task::Waker and Context for notifying executors when asynchronous tasks become ready.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Asynchronous futures (Future::poll) return Poll::Pending when I/O operations are not ready. If executors constantly polled futures in a busy-wait loop, CPU usage would spike to 100%.
Waker is a thread-safe handle that signals the async runtime executor when an I/O resource becomes ready. Context wraps the &Waker reference passed to Future::poll(). When I/O events complete, hardware or reactor threads call waker.wake(), prompting the executor to re-poll that specific task.
(2) Reality Metaphor
An airport boarding gate waiting area: instead of passengers lining up and asking the gate agent every 10 seconds if the plane is ready (busy polling), passengers sit down and wait until the loudspeaker announcement calls their group (waker.wake()).
(3) Rust Code Examples
Short Snippet
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
Fuller Example
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
pub struct ReadyFuture(pub i32);
impl Future for ReadyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(self.0)
}
}
fn main() {
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut fut = ReadyFuture(42);
let mut pinned = Pin::new(&mut fut);
assert_eq!(pinned.poll(&mut cx), Poll::Ready(42));
}
4. Common Mistakes & Pitfalls
Mistake 1: Forgetting to Clone and Save the Waker in Poll::Pending Futures
The mistake: Returning Poll::Pending from a custom Future::poll without calling cx.waker().clone() and saving it for the I/O event thread.
Why it is wrong: The executor puts the task to sleep and will never poll the future again because no Waker was saved to notify it. The task deadlocks permanently.
Incorrect:
fn poll(...) -> Poll<()> { Poll::Pending } // Saved no Waker! Task frozen forever!
Fix:
fn poll(..., cx: &mut Context) -> Poll<()> { self.waker = Some(cx.waker().clone()); Poll::Pending }
Mistake 2: Calling waker.wake() Inside the Busy Polling Loop
The mistake: Calling waker.wake() synchronously inside Future::poll before returning Poll::Pending.
Why it is wrong: Triggers an infinite spinning CPU loop where the executor immediately re-polls the future without any actual delay.
Incorrect:
fn poll(...) { cx.waker().wake_by_ref(); return Poll::Pending; } // Busy spin loop!
Fix:
Call waker.wake() asynchronously from an I/O completion event thread!
Mistake 3: Reusing Stale Waker Handles Across Multiple Polls
The mistake: Saving the Waker from the first poll() call and ignoring updated Waker instances in subsequent poll() calls.
Why it is wrong: Tasks may be moved between different executor worker threads; using a stale Waker notifies the wrong executor queue.
Incorrect:
if self.waker.is_none() { self.waker = Some(cx.waker().clone()); }
Fix:
Always update saved wakers: self.waker = Some(cx.waker().clone());
5. Practice Exercises
Exercise 1: Custom Timer Future with Waker Signal
Scenario: Build a custom asynchronous timer future TimerFuture that returns Poll::Pending on first poll, spawns a thread to sleep, and calls waker.wake() upon timer expiration.
Requirements:
- Define
TimerFuturestruct holdingshared_state: Arc<Mutex<State>>. - Implement
FutureforTimerFuture. - In
poll(), if not ready, savecx.waker().clone()and spawn timer thread. - Timer thread sleeps and calls
waker.wake(). - Write unit test.
Answer
Implementation
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::Duration;
pub struct SharedState {
pub completed: bool,
pub waker: Option<Waker>,
}
pub struct TimerFuture {
shared_state: Arc<Mutex<SharedState>>,
}
impl TimerFuture {
pub fn new(duration: Duration) -> Self {
let shared_state = Arc::new(Mutex::new(SharedState {
completed: false,
waker: None,
}));
let thread_state = shared_state.clone();
thread::spawn(move || {
thread::sleep(duration);
let mut guard = thread_state.lock().unwrap();
guard.completed = true;
if let Some(waker) = guard.waker.take() {
waker.wake();
}
});
Self { shared_state }
}
}
impl Future for TimerFuture {
type Output = &'static str;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut guard = self.shared_state.lock().unwrap();
if guard.completed {
Poll::Ready("Timer Expired")
} else {
guard.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timer_future_waker() {
let mut timer = TimerFuture::new(Duration::from_millis(10));
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pinned = Pin::new(&mut timer);
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Pending);
std::thread::sleep(Duration::from_millis(20));
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Ready("Timer Expired"));
}
}
Technical Explanation
- Demonstrates
Wakerlifecycle: savingcx.waker().clone()when returningPoll::Pending. - Timer thread invokes
waker.wake()to notify the executor.
Exercise 2: Mock Signal Waker Trigger
Scenario: Build a manual event signal SignalFuture woken by an external fire() call.
Requirements:
- Implement
SignalFuture. - Call
fire()and verifyPoll::Ready.
Answer
Implementation
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
pub struct SignalState {
pub fired: bool,
pub waker: Option<Waker>,
}
pub struct SignalFuture {
pub state: Arc<Mutex<SignalState>>,
}
impl SignalFuture {
pub fn new() -> (Self, Arc<Mutex<SignalState>>) {
let state = Arc::new(Mutex::new(SignalState { fired: false, waker: None }));
(Self { state: state.clone() }, state)
}
}
impl Future for SignalFuture {
type Output = u32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut guard = self.state.lock().unwrap();
if guard.fired {
Poll::Ready(100)
} else {
guard.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_waker() {
let (mut fut, state) = SignalFuture::new();
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pinned = Pin::new(&mut fut);
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Pending);
{
let mut guard = state.lock().unwrap();
guard.fired = true;
if let Some(w) = guard.waker.take() {
w.wake();
}
}
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Ready(100));
}
}
Technical Explanation
- Demonstrates event-driven asynchronous reactor notification pattern.
Exercise 3: Noop Waker Context Test Runner Helper
Scenario: Demonstrate constructing synchronous contexts using Waker::noop().
Requirements:
- Create
ContextfromWaker::noop().
Answer
Implementation
use std::task::{Context, Waker};
pub fn create_test_context<'a>(waker: &'a Waker) -> Context<'a> {
Context::from_waker(waker)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_noop_context() {
let waker = Waker::noop();
let cx = create_test_context(&waker);
assert!(cx.waker().will_wake(&waker));
}
}
Technical Explanation
Waker::noop()provides non-allocating test wakers for polling futures.
6. Related Terms
FutureTrait — Polling futures via Waker.- Executor / Runtime — Task executors.
7. Key Takeaways
Wakernotifies async runtime executors when tasks are ready for polling.Contextwraps the&Wakerpassed toFuture::poll().- Always clone and save
cx.waker()when returningPoll::Pending. - Call
waker.wake()from reactor or I/O completion threads to wake sleeping tasks.