---
title: Executors
source: 'https://academia.sh/en/courses/rust-concurrency/executors'
course: 'Concurrency and Asynchronous Rust'
language: en
updated: '2026-09-15T04:58:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that an executor is a component that decides only when a task is polled, independent of what the task does'
    - 'Runs the same task type through two different executor implementations and confirms that the poll count does not change'
    - 'Explains the difference between this lesson''s "who advances it" question and the axis the Traits, Generics and Error Handling course measured — how many times a body ran'
---

# Executors

Rust's standard library has no executor; the course's own two hand-written executor implementations produce identical poll counts when they run the same task — an executor adds nothing to a task's body, it only decides when to poll it.

The previous two lessons used `run` as a given tool: a future went in, and the result and the
poll count came back out. The mechanism in between was never questioned. This lesson opens up
exactly that mechanism. Rust's standard library has no executor — `Future`, `Pin`, `Context`,
`Poll`, and `Waker` define a contract, but they make no decision at all about who calls that
contract, or when. The code filling this gap sits hand-written in the course's measurement
core, and this lesson takes it as its direct subject.

This gap is not a shortcoming of the language, it is a deliberate boundary. An executor's
right decision depends on which program is running: a program with a small number of
long-running tasks wants one kind of scheduling, a program with thousands of short tasks wants
another. Rather than impose a single policy, the language standardizes the contract (`Future`,
`Poll`, `Waker`) and leaves scheduling to a separate component. This lesson builds two
different executors and runs the same tasks through both, to show what this separation means
in the source: swapping the executor does not change a single line of the code defining the
task.

## The Executor Is the Part Left Outside the Contract

The `Future` trait declares only the `poll` method; it says nothing anywhere about who calls
this method, in what order, or how often. This gap is deliberate: the language itself does not
impose a single scheduling policy, it leaves scheduling to a separate component — the
executor. The following excerpt defines two different executors. The first is `run`, used in
the previous lessons — an implementation that waits on a condition variable until woken. The
second is `run_loop` — an implementation that never waits, re-polling immediately as soon as
it gets `Pending` back.

```rust
// advance.rs — a slice of the core: two different executor implementations, the same task types.

use std::future::Future;
use std::pin::Pin;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Wake, Waker};

pub struct Notifier {
    pub ready: Mutex<bool>,
    pub condvar: Condvar,
}

impl Wake for Notifier {
    fn wake(self: Arc<Self>) {
        *self.ready.lock().unwrap() = true;
        self.condvar.notify_one();
    }
}

// executor 1: waits until woken
pub fn run<F: Future>(task: F) -> (F::Output, u32) {
    let mut task = Box::pin(task);
    let n = Arc::new(Notifier {
        ready: Mutex::new(false),
        condvar: Condvar::new(),
    });
    let waker: Waker = n.clone().into();
    let mut context = Context::from_waker(&waker);
    let mut poll_count = 0;
    loop {
        poll_count += 1;
        match task.as_mut().poll(&mut context) {
            Poll::Ready(x) => return (x, poll_count),
            Poll::Pending => {
                let mut ready = n.ready.lock().unwrap();
                while !*ready {
                    ready = n.condvar.wait(ready).unwrap();
                }
                *ready = false;
            }
        }
    }
}

pub struct EmptyNotifier;

impl Wake for EmptyNotifier {
    fn wake(self: Arc<Self>) {}
}

// executor 2: does not wait for the wake signal, immediately re-polls
pub fn run_loop<F: Future>(task: F) -> (F::Output, u32) {
    let mut task = Box::pin(task);
    let n = Arc::new(EmptyNotifier);
    let waker: Waker = n.clone().into();
    let mut context = Context::from_waker(&waker);
    let mut poll_count = 0;
    loop {
        poll_count += 1;
        if let Poll::Ready(x) = task.as_mut().poll(&mut context) {
            return (x, poll_count);
        }
    }
}

pub struct PausesOnce {
    pub paused: bool,
    pub name: &'static str,
    pub channel: Sender<String>,
}

impl Future for PausesOnce {
    type Output = &'static str;
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<&'static str> {
        if self.paused {
            self.channel.send(self.name.to_string()).unwrap();
            Poll::Ready(self.name)
        } else {
            self.paused = true;
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

pub struct ImmediatelyReady {
    pub name: &'static str,
    pub channel: Sender<String>,
}

impl Future for ImmediatelyReady {
    type Output = &'static str;
    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<&'static str> {
        self.channel.send(self.name.to_string()).unwrap();
        Poll::Ready(self.name)
    }
}
```

Both executors follow the same three steps: increment the poll counter, call `poll`, evaluate
the result. The only place they differ is what they do on a `Pending` return. `run` genuinely
waits on the condition variable that `Notifier` carries, and only resumes once `wake` is
called. `run_loop` uses a type named `EmptyNotifier`, whose `wake` call does nothing, and it
calls `poll` again without waiting every time it gets `Pending`. Both conform to the same
`Future` contract; both are valid executors. The difference between them is not in the task
itself, it is in **when** they choose to poll it again.

The difference between the two `Wake` implementations reflects this same choice. `Notifier`
carries a logical flag and a condition variable, genuinely delivering the `wake` call to the
waiting side — whoever calls it, `run` will see this call and wake up. `EmptyNotifier`, by
contrast, never uses `wake` at all; its body is empty, because `run_loop` already re-polls
without waiting for the wake call. The `Waker` itself is still handed to both future types —
`PausesOnce`'s `poll` calls `wake_by_ref` unconditionally, without knowing which executor's
`Wake` implementation its `Context` carries. Conforming to the contract demands exactly this
much; anything beyond that is the executor's own decision.

Both executors hold their own notifier inside an `Arc`. The shared ownership the Ownership and
the Memory Model course measured finds exactly its use here: the `Waker` passes into the
future's `poll` method as a reference from within `Context::from_waker`, but the notifier
itself also needs to go on living in the executor's own loop. `Arc` lets the notifier exist
inside the `Waker` and in the executor's own variable at the same time — whichever is dropped
first, the other can keep using its own copy. `EmptyNotifier` follows the same arrangement,
even though the data it holds is empty.

## Same Task, Two Executors, Same Poll Count

The following program constructs both a task that pauses once and one that is ready
immediately, from the same definitions, and runs each of them through two separate executors.

```rust
// two-executors.rs — the same task type, two different executors, the same poll count.
mod advance;
use advance::{run, run_loop, PausesOnce, ImmediatelyReady};
use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel::<String>();

    let (value_a1, poll_count_a1) = run(PausesOnce {
        paused: false,
        name: "pauses-run",
        channel: sender.clone(),
    });
    let (value_a2, poll_count_a2) = run_loop(PausesOnce {
        paused: false,
        name: "pauses-loop",
        channel: sender.clone(),
    });

    let (value_b1, poll_count_b1) = run(ImmediatelyReady {
        name: "ready-run",
        channel: sender.clone(),
    });
    let (value_b2, poll_count_b2) = run_loop(ImmediatelyReady {
        name: "ready-loop",
        channel: sender.clone(),
    });

    drop(sender);
    let mut records: Vec<String> = receiver.try_iter().collect();
    records.sort();

    println!(
        "pauses once:       run={} run_loop={} (value {} / {})",
        poll_count_a1, poll_count_a2, value_a1, value_a2
    );
    println!(
        "ready immediately: run={} run_loop={} (value {} / {})",
        poll_count_b1, poll_count_b2, value_b1, value_b2
    );
    println!(
        "same poll count: pauses={} ready={}",
        poll_count_a1 == poll_count_a2,
        poll_count_b1 == poll_count_b2
    );
    println!();
    println!("records ({}):", records.len());
    for record in &records {
        println!("  {}", record);
    }
}
```

```
pauses once:       run=2 run_loop=2 (value pauses-run / pauses-loop)
ready immediately: run=1 run_loop=1 (value ready-run / ready-loop)
same poll count: pauses=true ready=true

records (4):
  pauses-loop
  pauses-run
  ready-loop
  ready-run
```

**AP4.** Four separate calls run two different task types through two different executors, and
the result stays the same: the task that pauses once finishes in two polls on both executors,
the task that is ready immediately finishes in one poll on both. This stayed identical across
five separate runs. `PausesOnce`'s body never changed — it does not even know which executor
is calling it, it does nothing but call the `Waker` inside its `Context`. What determines the
poll count is the task's own `poll` logic; the executor only decides **how often** to trigger
that logic.

## How Many Tasks Run at the Same Time

Every call to `run` and `run_loop` takes a single root future and does not release the calling
thread until that future finishes. This is a simplified version of what a real executor
generally does: an executor usually holds a queue of more than one task at once, deciding which
one to poll by selecting from that queue. The four calls in this lesson — the `PausesOnce` and
`ImmediatelyReady` instances run through each of the two executors — run independently, one
after another; none of them is ever polled at the same time as another. Whether an executor
watches **more than one** task at once is a separate design question, and it falls outside this
lesson's measurement — what is fixed here is the plainest form of advancing a single task. The
two executor implementations producing the same number is already enough to show that, even in
this plain form, the executor's choice adds nothing to the body. The new question that arises
once an executor starts watching more than one task at once — which one gets polled first, how
long one keeps another waiting — does not change the single-task number this lesson measures;
it is a separate question, one concerning only the **order** between tasks, and it falls
outside this lesson.

## An Executor Decides When to Poll, Not What Happens

The difference between `run` and `run_loop` never shows up in the **observable** part of the
run: the same values, the same poll counts, the same set of records. The difference lives only
in the — unprinted, and this course forbids printing it — time that passes between a `Pending`
return and the next `poll` call. `run` genuinely puts the calling thread to sleep for that
time; `run_loop` keeps that same thread in a busy loop. Both produce the correct result, but
one releases the processor while waiting, the other does not. This is the same question the
Threads topic measured — what a thread does while it waits — reappearing here at the executor
level; but this lesson does not measure it through **duration**, it measures it through
**whether the poll count changes**, and shows that it does not.

The Traits, Generics and Error Handling course measured how many times a body ran — every
time, only when needed, or never. This lesson asks a different question: **who** advances a
body that runs the same number of times. The executor is exactly the answer to that question —
a subject that has no say in how many times a body runs, only in when those runs happen.
`run` and `run_loop` play this subject role in two different ways, yet both trigger the same
number of times.

This choice between the two implementations is not trivial in a real program.
`run_loop` keeps the processor constantly busy with its busy loop; the moment a task returns
`Pending`, even if it is not ready, the loop keeps querying it again and again. `run`, by
contrast, releases the processor while it waits on the condition variable and only comes back
once `wake` is called. Neither changes the poll count measured in this lesson, because
`PausesOnce`'s decision to return `Pending` is already made by looking at the `paused` field —
it gives the same answer regardless of how often it is asked from outside. In a future genuinely
waiting on a resource (a reply from the network, data from a file), this difference becomes
even more pronounced, but it still has no effect at all on the number this lesson
measures — the next lesson models exactly such a resource.

## The Same Executor, Two Different Future Types

Both `run` and `run_loop` were written generically with the `F: Future` bound; this is why the
same function can be called for both `PausesOnce` and `ImmediatelyReady`, without changing a
single line of the body. The Traits, Generics and Error Handling course measured that a generic
body is generated separately by the compiler for each concrete type; the same thing happens
here — `run::<PausesOnce>` and `run::<ImmediatelyReady>` are two separate bodies the compiler
produces. This lesson does not return to that axis, but it benefits from its result: the
executor's code is written in one place, and it does not need to be rewritten for every new
future type. All four calls in `two-executors.rs` call the same two functions (`run`,
`run_loop`); none of them duplicates the executor's body.

## The Limiting Measurement

Changing the executor does not change the task's body. `PausesOnce`'s `poll` method does not
even look at which executor is calling it — it looks at the value of the `paused` field and
makes its own decision. The same task produces the same record in both implementations; the
difference lives only in the polling schedule, not in the produced value or the record. This
limit shows what an executor **cannot** change: what a task produces, in how many steps it
produces it, or which way it branches. An executor can only change the **timing** of the `poll`
call; beyond that it has no authority over a task at all.

This limit becomes more visible in situations where more than one task coexists on the same
executor: an executor can decide which task gets polled **first**, but it cannot interfere with
how many times `Pending` a task returns within its own `poll`. This distinction stays hidden
in this lesson's single-task examples, because there is no other task to determine the
order; but the rule already holds here too — `run` and `run_loop` follow very different
scheduling strategies, yet neither could change the decision that `PausesOnce` finishes in two
polls.

## Summary

- Rust's standard library has no executor; the loop that calls `poll` is hand-written.
- An executor is the component that decides what to do on a `Pending` return — waiting,
  re-polling immediately, or following some other strategy is the executor's choice.
- The same task type run through two different executors produces the same poll count: the
  executor adds nothing to the task's body.
- The executor is the **subject** that advances a body; unlike the Traits, Generics and Error
  Handling course's "how many times did it run" question, this lesson asks "who advanced it."
- All four tasks run in this lesson fall into the **when-polled** class: none of them advances
  on its own, all four need an executor's `poll` call — only which executor makes that call
  changes, not the class itself.
- The real difference between the two executors lives not in the run count, but in the
  behavior during the — unprinted — time between a `Pending` return and the next poll;
  `run` and `run_loop` were written generically (`F: Future`), so the same executor body works
  with every future type.

## Next Step

This lesson showed that an executor does not interfere with what a task **produces**, deciding
only **when** it is polled; two utterly different scheduling strategies produced an identical
poll count on the same task. Up to this point, every task was either ready immediately or
paused once and resumed on its own — in both cases, the reason for pausing was the task's own
logic, not dependent on an outside condition. The next lesson takes up the case where an
outside resource — input or output — is not yet ready: a resource that is not ready returns
`Pending`, and wakes the executor once it is ready. What is measured is still the poll count,
but this time that number changes depending on how long the resource takes to become ready,
and this lesson reuses one of the two executors built here — `run`.
