Lesson 3 of 44
Your First Tokio Program
This lesson creates the smallest useful Tokio programs and explains each piece: the Cargo.toml entry, the feature flags, #[tokio::main], Runtime::new, block_on, the two runtime flavors, and #[tokio::test]. Afterwards you can start a runtime three different ways and know what each one sets up.
| Item | Purpose |
|---|---|
tokio with features = ["full"] |
turns on every stable Tokio component |
#[tokio::main] |
starts a runtime and runs an async fn main on it |
Builder, Runtime::new, block_on |
the code that the macro generates |
flavor = "current_thread", worker_threads |
choose the scheduler and its thread count |
#[tokio::test] |
runs an async test on its own runtime |
tokio::time::sleep |
an async pause that does not block a thread |
Example 1: Add Tokio to a project
cargo add writes the dependency line. The full feature turns on all stable components.
cargo new hello-tokio
cd hello-tokio
cargo add tokio --features fullThe resulting entry in Cargo.toml looks like this (the version is whatever is current):
[dependencies]
tokio = { version = "1", features = ["full"] }A feature flag is a Cargo switch that compiles an optional part of a crate. Tokio has no default features (default = [] in its Cargo.toml), so a plain tokio = "1" compiles a crate with almost nothing in it. Example 12 lists the features.
Example 2: The smallest Tokio program
async fn main cannot run by itself, because Rust has no built-in runtime (Example 11 shows the compiler error). The #[tokio::main] attribute wraps the function so that it starts a runtime and runs the body on it.
#[tokio::main]
async fn main() {
println!("before the pause");
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("after the pause");
}before the pause
after the pausetokio::time::sleep returns a future that completes after the given duration. .await waits for it. While the future is pending, the thread is free to run other tasks. This program has no other tasks, so it only pauses. The syntax of async and .await is explained in Futures and async/await.
Example 3: What #[tokio::main] expands to
The macro removes async from main and moves the body into a call on a runtime. The source of tokio-macros (src/entry.rs) generates a Builder::new_multi_thread() chain followed by .enable_all(), .build(), and .block_on(body). Written by hand, the program from Example 2 is:
use std::time::Duration;
use tokio::runtime::Builder;
fn main() {
Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async {
println!("before the pause");
tokio::time::sleep(Duration::from_millis(10)).await;
println!("after the pause");
});
}before the pause
after the pauseThe pieces:
| Call | Effect |
|---|---|
Builder::new_multi_thread() |
creates a builder for the multi-thread scheduler (needs feature rt-multi-thread) |
.enable_all() |
turns on the I/O driver and the time driver (shorthand for enable_io and enable_time) |
.build() |
creates the Runtime, including its worker threads, and returns io::Result<Runtime> |
.block_on(future) |
runs the future to completion on the current thread and returns its output |
A driver is the part of the runtime that watches for an external event. The I/O driver watches sockets and files, and the time driver watches timers. Both are explained in The Reactor and the Timer Wheel.
Example 4: Runtime::new
Runtime::new() is a shortcut. Its source is Builder::new_multi_thread().enable_all().build(), so it creates the same runtime as Example 3.
use tokio::runtime::Runtime;
fn main() {
let runtime = Runtime::new().unwrap();
runtime.block_on(async {
println!("running inside Runtime::new");
});
}running inside Runtime::newmain here is an ordinary synchronous function. The runtime is a value, and it stops when it is dropped at the end of main.
Example 5: block_on returns a value
block_on blocks the calling thread until the future finishes, then returns the future's output. The calling thread is not a runtime worker, so this is the way for synchronous code to call async code.
use tokio::runtime::Runtime;
async fn add_later(a: u32, b: u32) -> u32 {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
a + b
}
fn main() {
let runtime = Runtime::new().unwrap();
let sum = runtime.block_on(add_later(2, 3));
println!("sum = {sum}");
}sum = 5block_on panics if it is called from inside asynchronous code that a runtime is already running. Runtime Flavors and the Builder covers that case.
Example 6: One runtime, several block_on calls
A Runtime can run many futures, one block_on call after another. This is how a synchronous program (a command-line tool, for example) uses async libraries in a few places.
use tokio::runtime::Runtime;
fn main() {
let runtime = Runtime::new().unwrap();
let first = runtime.block_on(async { "first future" });
let second = runtime.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
"second future"
});
println!("{first}");
println!("{second}");
}first future
second futureThe runtime stays alive between the calls, so its worker threads and drivers are created once.
Example 7: Returning Result from main
#[tokio::main] keeps the return type of main. An async fn main that returns Result works with ? in the same way as an ordinary main.
use std::error::Error;
async fn parse_port(text: &str) -> Result<u16, std::num::ParseIntError> {
text.parse::<u16>()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let port = parse_port("8080").await?;
println!("port = {port}");
Ok(())
}port = 8080If main returned an Err, the process would print the error and exit with a non-zero status, as in a non-async program.
Example 8: The current-thread flavor
A flavor is the kind of scheduler a runtime uses. Tokio has two. The multi-thread flavor runs tasks on several worker threads and is the default for #[tokio::main]. The current-thread flavor runs every task on the one thread that calls block_on. It needs only the rt feature.
The macro selects it with flavor = "current_thread". The program reports how many workers each runtime has through Handle::current().metrics().num_workers(). (Handle and metrics are explained in Runtime Flavors and the Builder; here num_workers is only a way to read the worker count.)
use tokio::runtime::Handle;
#[tokio::main(flavor = "current_thread")]
async fn main() {
let workers = Handle::current().metrics().num_workers();
println!("current_thread workers: {workers}");
}current_thread workers: 1The hand-written equivalent uses Builder::new_current_thread():
use tokio::runtime::{Builder, Handle};
fn main() {
let runtime = Builder::new_current_thread().enable_all().build().unwrap();
runtime.block_on(async {
let workers = Handle::current().metrics().num_workers();
println!("current_thread workers: {workers}");
});
}current_thread workers: 1A current-thread runtime suits small programs, tests, and designs that keep one runtime per thread. Two consequences follow from having one thread: tasks never run in parallel, and a task that blocks the thread stalls all other tasks. Blocking and CPU-Bound Work demonstrates the second one.
Example 9: The default worker count and worker_threads
The multi-thread flavor starts one worker per CPU core unless told otherwise. The tokio-macros documentation states that worker_threads "defaults to the number of cpus on the system". The number of cores is read with std::thread::available_parallelism. When the environment variable TOKIO_WORKER_THREADS is set, its value is used instead, and the output below assumes it is not set.
use tokio::runtime::Handle;
#[tokio::main]
async fn main() {
let workers = Handle::current().metrics().num_workers();
let cpus = std::thread::available_parallelism().unwrap().get();
println!("workers equal the CPU count: {}", workers == cpus);
}workers equal the CPU count: trueworker_threads = N overrides the count:
use tokio::runtime::Handle;
#[tokio::main(worker_threads = 2)]
async fn main() {
let workers = Handle::current().metrics().num_workers();
println!("workers: {workers}");
}workers: 2worker_threads applies to the multi-thread flavor only. The builder method has the same name, and the macro calls .worker_threads(2) on the builder.
Example 10: Sequential awaits run in program order
std::thread::sleep stops the calling thread. tokio::time::sleep returns a future, and while that future is pending the runtime can use the thread for other tasks. A program with one task shows no difference, because nothing else is waiting to run. What it does show is ordering: each .await finishes before the next line runs.
use std::time::Duration;
use tokio::time::sleep;
async fn step(name: &str, millis: u64) {
println!("{name}: start");
sleep(Duration::from_millis(millis)).await;
println!("{name}: done");
}
#[tokio::main]
async fn main() {
step("first", 20).await;
step("second", 5).await;
println!("both steps finished, in program order");
}first: start
first: done
second: start
second: done
both steps finished, in program orderSequential .await calls run one after another, so the shorter sleep does not overtake the longer one. Running the two steps at the same time needs join! (join!, try_join!, and Running Work Concurrently) or spawned tasks (Spawning Tasks).
Example 11: async fn main without the attribute
Rust does not run async fn main on its own. The compiler rejects it with error E0752.
async fn main() {
println!("never runs");
}error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:1:1
|
1 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`The attribute is what turns the program into one that compiles: it removes async from main and calls block_on, as in Example 3.
Example 12: Feature flags and drivers
Each Tokio item is compiled only when its feature is on. Using an item whose feature is off is a compile error, because the item does not exist in the build. The tokio crate documentation lists the flags:
| Feature | Enables |
|---|---|
rt |
tokio::spawn, the current-thread scheduler, and non-scheduler utilities |
rt-multi-thread |
the multi-threaded, work-stealing scheduler (implies rt) |
macros |
#[tokio::main] and #[tokio::test] |
time |
tokio::time and the timer for the schedulers |
net |
tokio::net types such as TcpStream, UnixStream, and UdpSocket |
io-util |
the AsyncReadExt and AsyncWriteExt extension traits |
io-std |
Stdout, Stdin, and Stderr types |
sync |
all of tokio::sync |
fs |
tokio::fs |
process |
tokio::process |
signal |
tokio::signal |
parking_lot |
uses the parking_lot crate's locks internally (an optimization) |
full |
all of the above except test-util and unstable features |
test-util |
testing support such as the paused clock |
full is the list in the [features] table of Cargo.toml inside the tokio crate: fs, io-util, io-std, macros, net, parking_lot, process, rt, rt-multi-thread, signal, sync, and time. AsyncRead and AsyncWrite themselves need no feature. A service can list only the features it uses to compile less code. The course examples use full plus test-util where a lesson needs it.
Features and drivers are separate. The macro and Runtime::new call enable_all, but a hand-built runtime turns on only what the builder is told to. A timer used on a runtime without the time driver panics. The program below builds a current-thread runtime without enable_time and calls sleep:
use std::time::Duration;
use tokio::runtime::Builder;
fn main() {
let runtime = Builder::new_current_thread().build().unwrap();
runtime.block_on(async {
tokio::time::sleep(Duration::from_millis(1)).await;
});
}thread 'main' panicked at src/main.rs:7:9:
A Tokio 1.x context was found, but timers are disabled. Call `enable_time` on the runtime builder to enable timers.The message names the fix: call enable_time (or enable_all) on the builder.
Example 13: A basic #[tokio::test]
#[tokio::test] turns an async fn into a normal #[test] function that builds a runtime and runs the body with block_on. Its default runtime is a current-thread runtime, and each test gets its own (macro documentation: "The default test runtime is single-threaded. Each test gets a separate current-thread runtime.").
async fn double_later(n: u32) -> u32 {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
n * 2
}
#[tokio::test]
async fn doubles_a_number() {
assert_eq!(double_later(21).await, 42);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn runs_on_the_multi_thread_flavor() {
assert_eq!(double_later(5).await, 10);
}running 2 tests
test doubles_a_number ... ok
test runs_on_the_multi_thread_flavor ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRun tests with cargo test. Options such as start_paused and mocked I/O are covered in Testing Async Code, and the paused clock is introduced in Time: sleep, timeout, and interval.
Choosing how to start a runtime
| Situation | Use |
|---|---|
An application whose whole main is async |
#[tokio::main] |
| A small program or a test that needs one thread | #[tokio::main(flavor = "current_thread")] or #[tokio::test] |
| Synchronous code that calls async code in places | Runtime::new() and block_on |
| A runtime with custom thread names, sizes, or hooks | Builder (Runtime Flavors and the Builder) |
Interview points
async fn mainneeds a runtime.#[tokio::main]builds one and callsblock_onon the body.- The macro generates
Builder::new_multi_thread().enable_all().build().unwrap().block_on(...). Withflavor = "current_thread"it usesnew_current_thread. Runtime::new()is the multi-thread builder withenable_all.block_onruns a future on the calling thread and returns its output. It panics when called from inside an async context.- The multi-thread flavor defaults to one worker per CPU core.
worker_threadsoverrides it.#[tokio::test]defaults to a current-thread runtime. - Tokio has no default features.
fullenables all stable ones and excludestest-util. - A hand-built runtime needs
enable_time(orenable_all) beforetokio::timeworks. tokio::time::sleepfrees the thread while it waits.std::thread::sleepblocks it.
Summary
- Add Tokio with
cargo add tokio --features full, or list the features the program needs. #[tokio::main]expands to a runtime build followed byblock_onon the function body.Runtime::new()builds a multi-thread runtime with the I/O and time drivers.block_onconnects synchronous code to async code and returns the future's output.flavor = "current_thread"runs everything on one thread.worker_threads = Nsets the multi-thread worker count.#[tokio::test]runs each async test on a new current-thread runtime by default.tokio::time::sleepneeds thetimefeature and an enabled time driver.
