← Week 1: Tokio Fundamentals

Day 6: Tasks, JoinSet, and CancellationToken

Phase 2 · Jun 15, 2026

← Week 1: Tokio Fundamentals

Agenda (2–3 hours)

  • Read (45 min): tokio::task::JoinSet documentation; tokio_util::sync::CancellationToken documentation
  • Study (45 min): Compare structured concurrency (JoinSet) to unstructured (spawn with dropped JoinHandle); what leaks?
  • Practice (45 min): Refactor a Vec<JoinHandle<_>> + join_all into JoinSet; add cancellation with CancellationToken
  • Challenge (30 min): Implement a connection pool that maintains exactly N connections and restarts any that fail, using JoinSet
← Week 1: Tokio Fundamentals

The Problem with Detached Tasks

// Detached task — fire and forget
tokio::spawn(async { do_work().await }); // JoinHandle dropped!

If the JoinHandle is dropped, the task keeps running but there's no way to:

  • Wait for it to finish
  • Propagate panics
  • Cancel it on shutdown

This is "orphaned task" leakage — common source of hard-to-debug shutdown hangs.

← Week 1: Tokio Fundamentals

JoinSet: Structured Concurrency

use tokio::task::JoinSet;

let mut set = JoinSet::new();

for i in 0..10 {
    set.spawn(async move { expensive_task(i).await });
}

// Wait for all, collecting results
while let Some(result) = set.join_next().await {
    match result {
        Ok(value) => println!("got: {}", value),
        Err(e) => eprintln!("task panicked: {}", e),
    }
}
// When JoinSet is dropped, all remaining tasks are aborted

JoinSet::drop() aborts all outstanding tasks — no orphan tasks possible.

← Week 1: Tokio Fundamentals

CancellationToken

use tokio_util::sync::CancellationToken;

let token = CancellationToken::new();
let child_token = token.child_token();

// In each task:
tokio::spawn(async move {
    tokio::select! {
        _ = child_token.cancelled() => {
            println!("shutting down");
        }
        result = do_work() => {
            println!("done: {:?}", result);
        }
    }
});

// Cancel all children
token.cancel();

child_token() creates tokens that are cancelled when the parent is cancelled. Compose them for nested task trees.

← Week 1: Tokio Fundamentals

Task Hierarchy Pattern

async fn run_server(token: CancellationToken) {
    let mut set = JoinSet::new();
    loop {
        tokio::select! {
            conn = listener.accept() => {
                let conn_token = token.child_token();
                set.spawn(handle_connection(conn, conn_token));
            }
            Some(_) = set.join_next() => {} // collect finished tasks
            _ = token.cancelled() => break,
        }
    }
    set.shutdown().await; // drain remaining tasks
}

This pattern: one parent token cancels all children; JoinSet ensures cleanup.

← Week 1: Tokio Fundamentals

Key Takeaways

  • Never drop a JoinHandle silently — use JoinSet or explicitly abort() on shutdown
  • JoinSet provides structured concurrency: tasks are bounded to the set's lifetime
  • CancellationToken with child_token() gives cooperative, hierarchical cancellation
  • The combination of JoinSet + CancellationToken is the standard Tokio shutdown pattern

Tomorrow: Challenge — concurrent TCP echo server with connection limits.