Back to Tokio
Goal of This Episode
Return from the hand-written runtime to Tokio: revisit tokio::spawn and JoinHandle, compare Tokio’s block_on with our hand-written one, and learn the runtime’s multithreaded / single-threaded flavors.
Main Text
You Already Understand the Underneath
Congratulations on surviving the hardest episodes! We hand-wrote an executor, reactor, Task, and JoinHandle from scratch, and dissected state machines and Pin. Tokio’s real implementation is of course far more sophisticated, but looking back at its API now, most of the terms and design trade-offs should feel familiar.
tokio::spawn and JoinHandle
tokio::spawn is the spawn we hand-wrote: wrap a Future into a Task, hand it to the runtime’s scheduler, and get back a JoinHandle:
extern crate tokio;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
21 * 2
});
let result = handle.await.expect("the background task panicked");
println!("result: {}", result);
}
(.awaiting Tokio’s JoinHandle returns a Result, since the background Task might panic — hence the expect here.)
How Tokio’s block_on Differs from Ours
Unlike our hand-written version, tokio::runtime::Runtime::block_on requires neither Send nor 'static. It simply runs the Future you give it to completion on the current calling Thread, never moving it elsewhere, so Send isn’t a concern.
There is also an important semantic difference: the block_on we hand-wrote from Episode 11 onward waits until every Task in the ready queue completes before returning. Tokio’s block_on instead “returns as soon as the Future you passed it completes,” without waiting for other background Tasks opened via tokio::spawn. Unfinished background Tasks stay on the runtime.
The one-line contrast: the hand-written version “finishes the whole batch before moving on”; Tokio “finishes the one I specified, then moves on.” So in Tokio, block_on returning only means your Future finished; Tasks you spawned may still be running. If the runtime then shuts down, those background Tasks never get to finish.
The Most Common Beginner Compile Error: Holding a Non-Send Value Across .await
tokio::spawn requires Future: Send, and whether a Future is Send depends on what it stores across .awaits. Holding a non-Send value such as Rc across an .await makes the whole Future non-Send, so it can’t be spawned:
extern crate tokio;
use std::rc::Rc;
async fn some_async() {}
#[tokio::main]
async fn main() {
tokio::spawn(async {
let rc = Rc::new(5);
some_async().await; // rc is held across the .await, and Rc isn't Send
println!("{}", rc);
});
}
The compiler says future cannot be sent between threads safely and points out that Rc<i32> is used across an .await.
Several fixes:
Use a Send substitute. Here, swap Rc<i32> for Arc<i32>, which is Send:
extern crate tokio;
use std::sync::Arc;
async fn some_async() {}
#[tokio::main]
async fn main() {
tokio::spawn(async {
let arc = Arc::new(5);
some_async().await;
println!("{}", arc);
});
}
Dispose of the non-Send value before the .await. Shrink its scope with {} so it’s dropped before the .await, and the state machine never holds it across:
extern crate tokio;
use std::rc::Rc;
async fn some_async() {}
#[tokio::main]
async fn main() {
tokio::spawn(async {
let n = {
let rc = Rc::new(5);
*rc
}; // rc is dropped at the end of this block — it never crosses the .await
some_async().await;
println!("{}", n);
});
}
(Explicitly calling drop(rc) before the .await achieves the same.)
#[tokio::main] flavors
Finally: #[tokio::main] defaults to the multithreaded runtime, but you can change it:
extern crate tokio;
// single-threaded runtime
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("I run on a single thread");
}
Or specify the number of worker Threads:
extern crate tokio;
// multithreaded, with 4 workers
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
println!("I have 4 worker threads");
}
The single-threaded runtime’s upside is that it pays no cross-Thread cost; the downside is no true parallelism.
Recap
tokio::spawnhands aFutureto the runtime and returns aJoinHandle(.awaityields aResult, since theTaskmay panic).- Unlike our hand-written
block_on, Tokio’s requires neitherSendnor'staticand returns as soon as the specifiedFuturecompletes instead of waiting for allTasks. - Holding a non-
Sendvalue such asRcacross an.awaitmakes theFuturenon-Sendand unspawnable; fix withArc, or scope /dropit away before the.await. #[tokio::main]defaults to multithreaded, adjustable viaflavor = "current_thread"orworker_threads = N; either way,tokio::spawnstill requires itsFutureand output to beSend + 'static.