Tokio tasks switch only at .await

Read the explanation
What if

Two tasks share one thread. They take turns, and every switch happens at an .await.

Predict first

Two tasks share one thread. Each prints 3 steps and awaits yield_now() after each. What happens?

012345678spawn a, ba.awaitb.awaitreturntask a: step 1, yieldstep 1task a: ready, waits for the threadreadytask a: step 2, yieldstep 2task a: step 3, yieldstep 3readytask b: step 1, yieldstep 1task b: step 2, yieldstep 2task b: ready, waits for the threadreadytask b: step 3, yieldstep 3

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
Step 1/6

main spawns two tasks. One thread runs them all, so only one runs at any moment.

1async fn work(name: &'static str) {2    for step in 1..=3 {3        println!("{name} step {step}");4        task::yield_now().await; // let other tasks run5    }6}7 8#[tokio::main(flavor = "current_thread")] // one thread runs every task9async fn main() {10    let a = tokio::spawn(work("a"));11    let b = tokio::spawn(work("b"));12    a.await.unwrap();13    b.await.unwrap();14}

Explanation

What just happened

#[tokio::main(flavor = "current_thread")] runs every task on one thread. Each task printed a step, then yield_now().await handed the thread back to Tokio, which ran another ready task. They took turns, and every switch happened at an .await.

This run printed a1 b1 b2 a2 a3 b3. b got two turns in a row: which ready task goes next is the scheduler’s choice.

Try the what-if switch

Why

Rust’s async is cooperative. A future runs until it returns Pending at an .await, and only then can the runtime run something else. Nothing interrupts a task in the middle.

Watch out

A long loop with no .await holds the thread just like a blocking call. See the next scenario.

Common questions

When does a Tokio task give up the thread?

Only at an .await that isn't ready yet, like yield_now().await or a sleep. Between two awaits, the task runs without interruption.

Do spawned tasks run in parallel?

Not on a current_thread runtime: one thread runs every task, so only one runs at a time. On the default multi-threaded runtime they can run on different worker threads.

In what order do ready tasks run?

That's up to Tokio's scheduler. Each task's own steps stay in order, but don't depend on which task goes next.

Verified: this timeline matches a real program, checked in CI on Rust 1.99 with Tokio 1.54. How we verify. Unclear or wrong? Tell us