Skip to content

← Async From First Principles step 7 of 25

Medium Framework

Implementing `Wake`: a real task waker and the lost-wakeup bug

Up to now the driver has been polling in a tight loop, which means a future could return Pending and forget to say why, and nobody noticed. Real runtimes do not work like that. They poll a task, and then they wait until someone tells them that task can make progress. Now you build the “tell them” half.

pub struct TaskWaker { pub id: usize, pub queue: Arc<Mutex<VecDeque<usize>>> }
pub fn drive<F: Future<Output = u32>>(fut: F) -> Option<u32>
pub fn run(polite: bool, pends: u32) -> Option<u32>

drive seeds a ready queue with task id 0, then loops: pop an id, poll the future, stop when it is Ready. If the queue drains before the future finishes, nobody is ever going to poll it again — return None.

You are given two futures that differ in exactly one line. Polite calls cx.waker().wake_by_ref() before returning Pending. Forgetful does not. run picks one and drives it. The test set asserts both outcomes:

future pends result
Polite 3 Some(4)
Forgetful 0 Some(1) — it never pends, so it never needs a wakeup
Forgetful 1 null

That null is the entire lesson. In production this bug does not print anything. It is a program sitting at 0% CPU, not deadlocked in any way a stack dump can show, simply never polled again. Here it is a value you can assert on.

Wake — a real Waker, no unsafe

std::task::Wake (stable since 1.51) exists so you do not have to touch RawWakerVTable:

pub trait Wake {
    fn wake(self: Arc<Self>);
    fn wake_by_ref(self: &Arc<Self>) { /* default: clones the Arc and calls wake */ }
}

Implement it for a type you own, and Waker::from(Arc::new(my_waker)) gives you a genuine Waker. Overriding wake_by_ref is worth doing: the default clones the Arc only to drop it again, and your version does not need to.

The bound that will confuse you. Wake itself has no supertraits, but the conversion does: impl<W: Wake + Send + Sync + 'static> From<Arc<W>> for Waker. A Waker is Send + Sync and may be invoked from any thread, so everything reachable from it must be too. Put an Rc<RefCell<..>> inside your waker — which is exactly what the starter does — and you get E0277: “Rc<RefCell<VecDeque<usize>>> cannot be sent between threads safely”. The error points at Waker::from, not at your struct, which is why it reads strangely the first time. Arc<Mutex<..>> is the fix.

The bug hiding in the obvious loop

This is idiomatic-looking and wrong:

while let Some(id) = self.queue.lock().unwrap().pop_front() {
    // ... poll the task, which calls wake_by_ref(), which locks the queue ...
}

The MutexGuard is a temporary of the scrutinee, so it stays alive for the whole loop body. The moment a polled future wakes itself, it tries to lock a mutex this thread already holds — and std::sync::Mutex is not reentrant. The fix is to make the pop its own statement so the guard is dropped at the semicolon:

let id = queue.lock().unwrap().pop_front()?;   // guard gone here

Hold locks for the shortest possible region. That advice sounds like folklore until the first time you write the while let version.

Keep a budget

A future that wakes itself on every poll and never finishes will spin forever. Real runtimes accept that; a grading harness should not. Bound the loop (10 000 polls is generous) and return None when it runs out, so a runaway future fails fast instead of eating the timeout.

Loading visualization…