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
Repair a small executor that polls tasks on one thread. The supplied
interleave function creates one task for each count:
pub fn interleave(counts: Vec<u32>) -> Vec<usize>
Task i appends its index to a shared log counts[i] times, awaiting the
supplied yield_now() after each append. With this executor's FIFO queue,
the tasks take turns in input order:
| Counts | Returned log |
|---|---|
[3, 2, 1] |
[0, 1, 2, 0, 1, 0] |
[2, 2] |
[0, 1, 0, 1] |
[0, 3] |
[1, 1, 1] |
[] or [0, 0] |
[] |
A zero count contributes no log entries but still creates a task. Indices
refer to the original positions in counts.
What to fix
Keep Executor::new, spawn, run, yield_now and interleave available.
The starter already supplies the queue, wakers, task creation and logging.
Its invalid move is in run:
let mut task = self.tasks[id].unwrap();
unwrap() consumes the Option, but this expression tries to move a
non-Copy value out of a vector element. Use Option::take to leave None
in the slot and obtain the task. If the slot was already empty, skip the id.
Restore the task only when its poll returns Pending; leave completed slots
empty so later wakeups cannot poll them again.
How the pieces fit together
| Piece | Purpose |
|---|---|
Vec<Option<Task>> |
Stores tasks at stable indices and marks empty or completed slots with None. |
Pin<Box<dyn Future<Output = ()>>> |
Stores different future types behind one type and keeps each future's allocation pinned while its handle moves. |
Arc<Mutex<VecDeque<usize>>> |
Shares the FIFO queue with wakers and protects queue operations. |
TaskWaker { id, queue } |
Adds its task's id to the back of the queue when woken. |
The futures need not be Send: this executor polls them on its calling
thread, allowing the supplied tasks to share an Rc<RefCell<_>> log. The
waker's shared queue uses thread-safe types, as required for constructing a
Waker from Arc<TaskWaker>.
Each queue iteration removes an id, takes its task, builds a context with that task's waker, and polls once. Taking the task out is the supplied design's ownership-transfer strategy; it is not a requirement of every executor.
Why these tasks take turns
On its first poll, YieldNow marks itself done, wakes its task and returns
Pending. On its next poll, it returns Ready(()). Waking appends the task
id behind other queued tasks, producing the example's interleaving.
After a task's final append, it still awaits one last yield. A later poll
completes that yield and finishes the task without appending again. A task
with count c therefore needs c + 1 polls here, including when c is zero.
This behavior comes from this future and queue, not a universal fairness
guarantee of async Rust.
Release the queue lock before polling
Do not combine queue removal and polling into this form:
while let Some(id) = self.queue.lock().unwrap().pop_front() {
// Polling here can call a waker that locks the same queue.
}
The temporary mutex guard in the while let condition remains alive through
the body. Relocking a standard mutex on the same thread is unsupported and
may deadlock or panic. The starter already avoids that problem:
let next = self.queue.lock().unwrap().pop_front();
let Some(id) = next else { return };
The guard is released at the first statement's semicolon. Polling can then call the waker without still holding the queue lock. Keep this separation.
Scope and limits
run() returns as soon as it sees an empty queue. That does not establish
that all futures completed or that no later wakeup is possible. An externally
woken future could become ready after run() returns; the caller would have
to run the executor again. There is no event loop that waits for I/O or timers.
Waking can enqueue an id more than once. Empty-slot checks handle stale ids after completion, but this implementation does not deduplicate notifications. A wakeup requests another poll; it does not guarantee that poll will complete.
The supplied guard allows 100,000 queue-loop iterations before panicking.
Stale ids count too, and this is not a wall-clock timeout: a single blocking
or non-returning poll can still stall it. For the supplied interleave tasks,
inputs must satisfy sum(counts) + counts.len() < 100_000, leaving an
iteration to observe the empty queue. Even completion on the final allowed
iteration reaches the current guard's panic.
This is a bounded teaching executor, not a general-purpose async runtime. The exercise exposes task ownership, waking and polling without requiring a runtime crate.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.