🎓 A1Learner
🔍
0 of 45 lessons completed

Rust Tokio Tasks

A Tokio task is a lightweight job run by the runtime. A task costs little more than a future, not a whole operating-system thread.

Spawn a task

spawn starts the job at once and hands back a JoinHandle:

let handle = tokio::spawn(async { 40 + 2 });

Collect the result with await

Awaiting the handle hands back whatever the task returned:

let answer = handle.await.unwrap();
println!("answer = {answer}");

Run two futures in one task

join! runs futures side by side inside one task and waits for both:

let (one, two) = tokio::join!(
    async { "one".len() },
    async { "two".len() }
);
println!("{} and {}", one, two);

Stop a task from a second handle

The JoinHandle stays with you for the verdict. The AbortHandle goes to someone else. It goes to whoever owns the stop button:

let task = tokio::spawn(async {
    sleep(Duration::from_secs(60)).await;
    9
});
let aborter = task.abort_handle();
aborter.abort();
match task.await {
    Ok(_) => println!("finished normally"),
    Err(e) => println!("cancelled = {}", e.is_cancelled()),
}

Warn the runtime before blocking

On a multi-thread runtime, block_in_place moves your other tasks out of the way before the blocking call runs. The call itself is ordinary sync code:

let sum = tokio::task::block_in_place(|| (1..=100u64).sum::<u64>());
println!("sum = {sum}");

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let first = tokio::spawn(async { "one".len() });
    let second = tokio::spawn(async { "two".len() });
    let third = tokio::spawn(async { "three".len() });

    let total = first.await.unwrap() + second.await.unwrap() + third.await.unwrap();
    println!("total letters = {}", total);
}

Key points

  • Await the JoinHandle to get the task's value.
  • It gives a Result.
  • A task can panic
  • A task must be Send + 'static: the runtime may move it between threads, and it must own what it uses
  • Tasks are cheap: each one costs little more than a future, not a whole OS thread
  • A JoinError tells you how a task ended: err.is_cancelled() is true after an abort, err.is_panic() after a panic — awaiting an aborted task gives you an Err to inspect, not a panic of your own
  • tokio::task::block_in_place(|| ...) tells a multi-thread runtime one thing.
  • It says "this thread is about to block".
  • The runtime reacts first.
  • It hands the thread's other tasks to a different thread.
  • It panics on a current-thread runtime — spawn_blocking stays the everyday answer
  • A program can run thousands of tasks at once.

In this example, three tasks are spawned, each counting the letters of one word. Each spawn returns a JoinHandle at once, so all three jobs are in flight together. Awaiting the handles one by one collects 3, 3, and 5. The sum is 11 — and the awaits did not slow the tasks down, because the work was already running.

Note

Dropping a JoinHandle does not stop the task — it keeps running, detached.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

You spawn a task and drop its JoinHandle without awaiting it. What happens?