Skip to content
academia.sh

Lesson 01 / 10

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close