We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 17 of 25
The minimal single-threaded executor
interleave(vec![3, 2, 1]) == [0, 1, 2, 0, 1, 0]
Three tasks. One thread. Each task appends its own index to a shared log and then yields. The output is round-robin — and that single line of data is proof that you are running concurrent tasks on one thread with no runtime crate anywhere in sight.
pub struct Executor { /* tasks + ready queue */ }
impl Executor {
pub fn new() -> Self;
pub fn spawn(&mut self, fut: impl Future<Output = ()> + 'static);
pub fn run(&mut self);
}
pub fn interleave(counts: Vec<u32>) -> Vec<usize>
interleave spawns one task per entry of counts; task i loops counts[i]
times, pushing i to the log and awaiting the supplied yield_now().
The three pieces, and that is genuinely all of them
A task list. Vec<Option<Pin<Box<dyn Future<Output = ()>>>>>, indexed by
task id. Boxed because tasks have different concrete types and must live at a
stable address; Option because a running task is temporarily taken out and
put back, and a finished task is left as None. Give it a type alias —
clippy::type_complexity is a default lint and that type is over the
threshold.
A ready queue. Arc<Mutex<VecDeque<usize>>> holding the ids of tasks that
can make progress right now. spawn pushes; run pops.
A per-task waker. TaskWaker { id, queue } with impl Wake, from item
16.7. This is the piece that makes it a scheduler rather than a loop: because
the waker carries the id, waking a task re-queues exactly that task rather
than “something changed, re-poll everything”.
run is then a loop: pop an id, take the task out of its slot, build that
task’s waker and Context, poll it once, and put it back if it returned
Pending. When the queue is empty, every remaining task is waiting on
something that will never happen, so stop.
The bug of this problem
This looks right and self-deadlocks on the first wakeup:
while let Some(id) = self.queue.lock().unwrap().pop_front() {
// poll the task here — and the task calls wake_by_ref(),
// which locks the queue this thread is already holding
}
The MutexGuard produced in a while let scrutinee lives for the whole loop
body. std::sync::Mutex is not reentrant. Make the pop its own statement so
the temporary is dropped at the semicolon:
let next = self.queue.lock().unwrap().pop_front();
let Some(id) = next else { return };
The general rule — hold a lock for the shortest region you can — is the same
one that will bite you again as clippy::await_holding_lock in item 16.22.
The error you are meant to meet
The starter reaches for the task with
let mut task = self.tasks[id].unwrap();
E0507: cannot move out of index of Vec<Option<Pin<Box<dyn Future ..>>>>.
Indexing gives you a place behind a &mut, and Option::unwrap consumes by
value. Option::take is the answer: it swaps None in and hands you the
contents, leaving the vector valid. Exactly the mem::replace move from item
16.12, with a nicer name.
Taking the task out is not merely a workaround, either. It is what lets you
poll the task while still holding &mut self for the executor — the task is no
longer inside self while it runs.
Details that will cost you an hour otherwise
-
Put a poll budget on
run. A task that wakes itself and never completes is a legal, easy-to-write bug. Bounding the loop turns an infinite hang into a fast, legible failure. -
#[derive(Default)]works here, since every field type has aDefault— and having aDefaultis also what stopsclippy::new_without_defaultfrom complaining about yournew(). -
A finished task must not be re-queued. Leaving the slot as
Noneand skipping ids that are alreadyNonehandles the case where a task woke itself immediately before completing.
When this passes, read tokio’s Runtime with the knowledge that it is this,
plus work stealing, plus an epoll loop, plus twelve years of tuning.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.