Explanation
What just happened
Both tasks ran on one thread. main called std::thread::sleep, which doesn’t .await: it put the whole thread to sleep. The ticker’s 20 ms were up, but there was no thread to run it on. tick 2 came only after slow call ends.
It compiles without a warning. Blocking calls look like any other function call.
Try the what-if switch
- spawn_blocking: the sleep runs on a separate pool thread (a
tokio-rt-worker).mainawaits it, which frees the runtime’s thread, so all the ticks print beforeslow call ends. - sleep().await:
tokio::time::sleepis an async wait. It hands the thread back while it waits, and the ticker keeps ticking.
Why
An async runtime runs many tasks on few threads. That only works if every task gives the thread back at its .awaits. One blocking call holds it for everyone.
Watch out
A long loop with no .await stalls the thread the same way, even without a single blocking call in it.
Common questions
Why is std::thread::sleep bad in async code?
It blocks the whole thread, not just the task. On a current_thread runtime no other task can run until it returns: in our run the ticker didn't tick once during the 100 ms sleep.
What does spawn_blocking do?
It runs the closure on a separate thread from Tokio's blocking pool and gives you a future for the result. Awaiting it frees the async thread for other tasks.
Should I use spawn_blocking or tokio::time::sleep?
To wait, use tokio::time::sleep(..).await. Use spawn_blocking for work that has to block, like a blocking library call or file I/O with std::fs.
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