C.W.K.
Stream
Lesson 04 of 05 · published

Spawning Tasks & Joining

~11 min · async, spawn, join, concurrency

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

A runtime lets you do more than await one Future at a time. tokio::spawn, joining, and join! are the tools for running many async tasks concurrently — the async analogues of threads and channels.

spawn: concurrent tasks

tokio::spawn takes a Future and runs it as an independent task on the runtime, returning a handle you can .await for its result. Unlike a thread, a task is cheap — the runtime multiplexes thousands onto a few threads. Spawning is how you get true concurrency: several tasks making progress while each waits on its own I/O.

Awaiting many at once

To wait for several Futures together, tokio::join! drives them concurrently and waits for all to finish — far faster than awaiting them one after another, because their waits overlap. tokio::select! instead waits for whichever Future finishes first, useful for timeouts and racing operations.

join! overlaps waits; sequential .await serializes them. Awaiting three 1-second I/O calls one by one takes 3 seconds; tokio::join! runs them concurrently and finishes in ~1. When tasks are independent, joining is the difference between concurrent and merely sequential async.

Tasks usually must be Send

Because tokio may move a task between worker threads, spawned Futures generally must be Send — the same trait from the Concurrency track. So the Arc/Mutex rules apply here too: share state across tasks with Arc, and the compiler keeps async sharing race-free just as it does for threads.

Code

spawn for independent tasks, join! to overlap waits·rust
#[tokio::main]
async fn main() {
    // spawn: independent concurrent tasks
    let h1 = tokio::spawn(async { slow_double(10).await });
    let h2 = tokio::spawn(async { slow_double(20).await });

    // join the handles for their results
    let (a, b) = (h1.await.unwrap(), h2.await.unwrap());
    println!("{a} {b}"); // 20 40

    // join!: drive multiple futures concurrently, overlapping their waits
    let (x, y) = tokio::join!(slow_double(1), slow_double(2));
    println!("{x} {y}"); // 2 4
}

async fn slow_double(n: i32) -> i32 { n * 2 }

External links

Exercise

Write three async functions that each tokio::time::sleep for a moment then return a number. Await them sequentially (one .await after another) and time it; then run them with tokio::join! and time that. Why is the join! version roughly as fast as the single slowest call?
Hint
Sequential awaits add up the waits; join! overlaps them, so total time is the slowest single future, not the sum. That overlap is the entire point of concurrent async.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.