---
title: 'Thread Creation'
source: 'https://academia.sh/en/courses/rust-concurrency/thread-creation'
course: 'Concurrency and Asynchronous Rust'
language: en
updated: '2026-09-15T04:58:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Distinguishes a unit's on-its-own, when-something-else-finishes, and not-at-all states from one another"
    - 'Verifies by running the measurement core how thirteen units split across four classes'
    - "Separates this course's measurement axis from the axes of earlier concurrency courses"
---

# Thread Creation

The course's opening lesson runs the full measurement core: of thirteen units, 5 advanced on their own, 3 when polled, 3 when something else finished; 2 never advanced at all — thread spawning and joining establish this distribution's on-its-own and when-something-else-finishes classes.

Throughout its fifteen lessons, the Traits, Generics and Error Handling course measured how
many times a body written in the source ran, and its closing said it was not dropping that
question but changing its direction. That closing sentence anticipated that the next question
would be "which thread ran it" — but that is the JVM, Concurrency and Performance course's own
question, and it was already answered there. This course asks a different question. A job,
even when it is written in the source, may need something in order to advance: a poller,
another unit finishing, or nothing at all. The one question asked throughout the course is:
**what does this job need in order to advance?**

This is the repository's fifth concurrency course. The previous four covered the same
territory with different questions: the Operating System Concepts course counted how many
distinct interleavings were possible, Python's Concurrency and Performance course measured
which steps overlapped within a tick model, the JVM, Concurrency and Performance course tracked
which thread ran a given step, and Go's Concurrency course classified how a goroutine could
stop. This course reuses none of their names: the concepts of a lock, a channel, a thread, a
data race, and a scheduler are already established and are not redefined here. Whenever a
related concept comes up, it will first be named by the course where it was established, what
was measured there will be said in one sentence, and the discussion will then return to this
course's own question.

## Four Classes

The question has four answers, and these four names do not change throughout the course.

| Class | Definition |
|---|---|
| on its own | Once spawned, it needs no one; it advances by itself |
| when polled | It does not advance on its own; a poller advances it |
| when something else finishes | It waits for another unit to finish |
| not at all | Written in the source, but never advanced |

This four-way split should not be confused with the JVM, Concurrency and Performance course's
classification: what was measured there was **which** thread ran a step; what is measured here
is **what** a unit needs in order to advance. It should not be confused with Go's Concurrency
course either: what was measured there was how a unit **stops** (an ending); what is measured
here is how it **advances** (a start and a continuation). The Operating System Concepts
course's interleaving count and Python's Concurrency and Performance course's tick model do
not overlap with this four-way split either: both of those counted **when** a step ran, while
this course asks **what** a unit needs in order to advance — no timing information is measured
anywhere. This lesson's subject is the on-its-own and when-something-else-finishes classes:
when a thread is spawned, does it advance on its own, or does the side waiting to join it
advance when something else finishes?

## Thread Spawning and Joining

The Operating System Concepts course established what a thread is and how the scheduler runs
it; this course does not re-explain that concept. In Rust, spawning a thread means handing off
a closure so that a separate unit of execution runs it. The spawn call returns a handle; when
join is called on that handle, the calling side waits for the thread to finish and gains access
to the value the thread returned.

The progress conditions of these two operations differ. The spawned thread itself needs no one
from the moment it starts running — it advances on its own for as long as the scheduler grants
it a share of the processor. The side that makes the join call, by contrast, cannot advance
until that thread finishes; this is the most direct example of the when-something-else-finishes
class. The same distinction holds for a thread waiting on a signal: waiting for word from a
channel is a different mechanism from calling join, but it falls into the same class, because
both wait for an event outside themselves to occur.

The join call itself returns a value: the value the spawned closure returned comes back through
the handle. This is a direct extension of the move rule established in the Ownership and the
Memory Model course — every value the closure captures is moved into that thread, and when the
closure returns the value it produced, that value is moved into the join call in turn. The
calling side cannot access that value before the join completes; if it could, it would be
trying to read a value the thread might not have produced yet. Join falling into the
when-something-else-finishes class is therefore not an implementation detail but the necessary
consequence of the ownership rule.

## The `not at all` Class: Written in the Source, Never Ran

Two units never advanced in this run, and both stand quite plainly in the source. The first is
a closure: it is bound under the name `_never_started`, written to send a record to the log
channel, but it is never called anywhere and never handed to a thread. In Rust, a closure is
only a value until it is called — its body compiles, but it is never triggered during the run.
The second is a task: a `PausesOnce` value named `task-never-polled` is constructed, but it is
never given to the `run` function; a future, like a closure, is only a value sitting in memory
until it is polled.

What these two share is that their presence in the source is not the same as their effect on
the run. All thirteen rows in the `classes` array are written in the source; the "record
present" column in the report table says "no" for only two of them. This does not mean the
source code contains dead code — the same closure type or the same task type could have been
called or polled in a different run. What is measured is not the possibility the source
carries, but which path was chosen in this specific run.

## The Measurement Core

The core that the rest of the course will take cross-sections of is `advance.rs`. It spawns
thirteen separate units in a single run, classifies them, and collects the records that arrive
over a channel. The records are written not into a hash structure but into an ordinary list,
and are sorted before they are read — which thread sent its record in which order is never
measured or printed anywhere in this course; only whether the record exists is measured.

```rust
// advance.rs — what a job needs in order to advance: no one, a poller, something else
// finishing, or it never advances at all.

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

// --- hand-written executor: the standard library has no executor ---

struct WakeSignal {
    ready: Mutex<bool>,
    condition: Condvar,
}

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

fn run<F: Future>(task: F) -> (F::Output, u32) {
    let mut task = Box::pin(task);
    let w = Arc::new(WakeSignal {
        ready: Mutex::new(false),
        condition: Condvar::new(),
    });
    let waker: Waker = w.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 r = w.ready.lock().unwrap();
                while !*r {
                    r = w.condition.wait(r).unwrap();
                }
                *r = false;
            }
        }
    }
}

// --- a task that pauses once and then continues ---

struct PausesOnce {
    paused: bool,
    name: &'static str,
    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
        }
    }
}

struct ImmediatelyReady {
    name: &'static str,
    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)
    }
}

async fn await_inner(inner: PausesOnce, channel: Sender<String>) -> &'static str {
    let _ = inner.await;
    channel.send(String::from("task-awaiting")).unwrap();
    "awaiting"
}

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

    // --- units that advance on their own: once spawned, they need no one ---
    for i in 1..=3 {
        let s = sender.clone();
        thread::spawn(move || {
            s.send(format!("thread-on-its-own-{}", i)).unwrap();
        });
    }

    // --- units waiting for something else to finish ---

    // (a) the main thread waits for a join
    let s = sender.clone();
    let handle = thread::spawn(move || {
        s.send(String::from("thread-joined")).unwrap();
    });
    handle.join().unwrap();
    sender.send(String::from("main-after-join")).unwrap();

    // (b) a thread waits for word from a channel
    let (signal_sender, signal_receiver) = channel::<()>();
    let s = sender.clone();
    let waiter = thread::spawn(move || {
        signal_receiver.recv().unwrap();
        s.send(String::from("thread-channel-waiter")).unwrap();
    });
    let s = sender.clone();
    thread::spawn(move || {
        s.send(String::from("thread-signaler")).unwrap();
        signal_sender.send(()).unwrap();
    });
    waiter.join().unwrap();

    // --- never started ---
    let s = sender.clone();
    let _never_started = move || {
        s.send(String::from("never-started")).unwrap();
    };

    // --- units that advance when polled ---
    let (_, poll_count_a) = run(PausesOnce {
        paused: false,
        name: "task-polled-a",
        channel: sender.clone(),
    });
    let (_, poll_count_b) = run(ImmediatelyReady {
        name: "task-ready-b",
        channel: sender.clone(),
    });

    // --- awaiting: does not advance until the inner one finishes ---
    let inner = PausesOnce {
        paused: false,
        name: "task-inner",
        channel: sender.clone(),
    };
    let (_, poll_count_c) = run(await_inner(inner, sender.clone()));

    // --- a task that is never polled ---
    let _never_polled = PausesOnce {
        paused: false,
        name: "task-never-polled",
        channel: sender.clone(),
    };

    // --- report: the expected number of records is read and sorted ---
    let expected = 3 + 2 + 2 + 2 + 2; // on its own 3, join 2, channel 2, polled 2, awaiting 2
    let mut records: Vec<String> = Vec::new();
    for _ in 0..expected {
        records.push(receiver.recv().unwrap());
    }
    records.sort();

    println!("recorded advances ({}):", records.len());
    for r in &records {
        println!("  {}", r);
    }

    println!();
    println!("poll counts: a={} b={} awaiting={}", poll_count_a, poll_count_b, poll_count_c);

    let classes: [(&str, &str); 13] = [
        ("thread-on-its-own-1", "on its own"),
        ("thread-on-its-own-2", "on its own"),
        ("thread-on-its-own-3", "on its own"),
        ("thread-signaler", "on its own"),
        ("thread-joined", "on its own"),
        ("main-after-join", "when something else finishes"),
        ("thread-channel-waiter", "when something else finishes"),
        ("task-awaiting", "when something else finishes"),
        ("task-polled-a", "when polled"),
        ("task-ready-b", "when polled"),
        ("task-inner", "when polled"),
        ("never-started", "not at all"),
        ("task-never-polled", "not at all"),
    ];

    println!();
    println!("{:<26}{:<30}{}", "unit", "class", "record present");
    for (name, class) in classes {
        let present = records.iter().any(|r| r == name);
        println!("{:<26}{:<30}{}", name, class, if present { "yes" } else { "no" });
    }

    println!();
    for c in ["on its own", "when polled", "when something else finishes", "not at all"] {
        let count = classes.iter().filter(|(_, k)| *k == c).count();
        println!("{:<30}{}", c, count);
    }
    println!("total units: {}", classes.len());
}
```

```
recorded advances (11):
  main-after-join
  task-awaiting
  task-inner
  task-polled-a
  task-ready-b
  thread-channel-waiter
  thread-joined
  thread-on-its-own-1
  thread-on-its-own-2
  thread-on-its-own-3
  thread-signaler

poll counts: a=2 b=1 awaiting=2

unit                      class                         record present
thread-on-its-own-1       on its own                    yes
thread-on-its-own-2       on its own                    yes
thread-on-its-own-3       on its own                    yes
thread-signaler           on its own                    yes
thread-joined             on its own                    yes
main-after-join           when something else finishes  yes
thread-channel-waiter     when something else finishes  yes
task-awaiting             when something else finishes  yes
task-polled-a             when polled                   yes
task-ready-b              when polled                   yes
task-inner                when polled                   yes
never-started             not at all                    no
task-never-polled         not at all                    no

on its own                    5
when polled                   3
when something else finishes  3
not at all                    2
total units: 13
```

**TH1.** This run is deterministic: across five separate runs the same thirteen units were
produced, in the same sorted list, with the same class distribution. **Of the thirteen units,
5 advanced on their own, 3 when polled, 3 when something else finished; 2 never advanced at
all.** This count does not change for the rest of the course; each lesson only takes its own
cross-section.

Three of the five `on its own` rows are the independent threads spawned by the loop, one is the
thread that sends the signal, and one is the thread spawned by the side waiting to join. What
these five share is that from the moment they are spawned to the moment they send their record,
they wait on no one. Two of the three `when something else finishes` rows are at the thread
level: the main thread cannot advance until the join call finishes, and the thread waiting on
the signal cannot advance until word arrives over the channel. The third, `task-awaiting`, is
not this lesson's subject — it is the outer layer of an asynchronous task and will be addressed
in the Asynchronous Programming topic. The three `when polled` rows belong to that same topic:
this lesson only notes that they, too, are present in the source and that their records appear
in the report table; it does not explain the polling mechanism itself.

## The Limits of the Measurement

The **order** of the records is never measured anywhere in this lesson: whatever order the
three independent threads send their records in, the report first collects them, then sorts
them, and only then prints them. The reason is not a shortcoming but a deliberate boundary: the
order in which the scheduler runs the three threads depends on the environment, and this course
prints no value that depends on the environment. What is measured is whether the expected
number of records **exists** — how many arrived and which names arrived — not their order.

This boundary also shows up in the core's `expected` constant: the report expects exactly
eleven records and reads exactly that many, neither one more nor one fewer. If a thread never
sent its record, the program would wait for it forever — which is why the two units in the
`not at all` class (the `never-started` closure and the `task-never-polled` task) never touch
the report channel at all; their absence of a record is measured by leaving them out of the
`expected` count, that is, by the program never waiting for them in the first place.

This design decision itself carries a lesson: how many units a program has to wait for is not
something discovered while the program runs, but a number known in advance from the source.
`expected` adds up the three records of the three independently spawned threads, the two
records of the join pair, the two records of the channel pair, the two records of the two
polled tasks, and the two records of the awaiting task — eleven in total. This total is a
hand-computed constant, not a value learned from the run; this is the method the rest of the
course will use: the expected number is first derived from the source, and only then is it
checked whether the run meets that number.

## Summary

- The course's measurement axis is what a unit needs in order to advance; four classes do not
  change throughout the course: on its own, when polled, when something else finishes, not at
  all.
- The measurement core classifies thirteen units: 5 advanced on their own, 3 when polled, 3
  when something else finished, 2 never advanced at all; this count does not change throughout
  the course.
- A spawned thread advances on its own from the moment it starts running; the side waiting to
  join it advances when something else finishes.
- The order in which records are sent is neither measured nor printed; what is measured is
  whether the expected number of records exists.
- The JVM, Concurrency and Performance course measured which thread ran a given step; this
  course measures what that same step needs in order to advance.

## Next Step

In this lesson, the thread waiting on a signal advanced through a signal channel; the next
lesson runs the same mechanism with a payload. A value that arrives from a channel together
with its ownership leaves the side that sends it on its own, while it keeps the side that
receives it waiting when something else finishes — the next lesson measures this pair with a
message-passing example.
