Guide contents

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.

Terminal
cargo new hello-tokio
cd hello-tokio
cargo add tokio --features full

The resulting entry in Cargo.toml looks like this (the version is whatever is current):

TOMLCargo.toml
[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.

Rust
#[tokio::main]
async fn main() {
    println!("before the pause");
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    println!("after the pause");
}
Output
before the pause
after the pause

tokio::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:

Rust
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");
        });
}
Output
before the pause
after the pause

The 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.

Rust
use tokio::runtime::Runtime;

fn main() {
    let runtime = Runtime::new().unwrap();
    runtime.block_on(async {
        println!("running inside Runtime::new");
    });
}
Output
running inside Runtime::new

main 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.

Rust
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}");
}
Output
sum = 5

block_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.

Rust
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}");
}
Output
first future
second future

The 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.

Rust
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(())
}
Output
port = 8080

If 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.)

Rust
use tokio::runtime::Handle;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let workers = Handle::current().metrics().num_workers();
    println!("current_thread workers: {workers}");
}
Output
current_thread workers: 1

The hand-written equivalent uses Builder::new_current_thread():

Rust
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}");
    });
}
Output
current_thread workers: 1

A 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.

Rust
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);
}
Output
workers equal the CPU count: true

worker_threads = N overrides the count:

Rust
use tokio::runtime::Handle;

#[tokio::main(worker_threads = 2)]
async fn main() {
    let workers = Handle::current().metrics().num_workers();
    println!("workers: {workers}");
}
Output
workers: 2

worker_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.

Rust
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");
}
Output
first: start
first: done
second: start
second: done
both steps finished, in program order

Sequential .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.

Rust✗ does not compile · E0752
async fn main() {
    println!("never runs");
}
Output
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:

Rustpanics at runtime
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;
    });
}
Output
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.").

Rustcargo test
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);
}
Output
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.00s

Run 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 main needs a runtime. #[tokio::main] builds one and calls block_on on the body.
  • The macro generates Builder::new_multi_thread().enable_all().build().unwrap().block_on(...). With flavor = "current_thread" it uses new_current_thread.
  • Runtime::new() is the multi-thread builder with enable_all.
  • block_on runs 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_threads overrides it. #[tokio::test] defaults to a current-thread runtime.
  • Tokio has no default features. full enables all stable ones and excludes test-util.
  • A hand-built runtime needs enable_time (or enable_all) before tokio::time works.
  • tokio::time::sleep frees the thread while it waits. std::thread::sleep blocks 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 by block_on on the function body.
  • Runtime::new() builds a multi-thread runtime with the I/O and time drivers.
  • block_on connects synchronous code to async code and returns the future's output.
  • flavor = "current_thread" runs everything on one thread. worker_threads = N sets the multi-thread worker count.
  • #[tokio::test] runs each async test on a new current-thread runtime by default.
  • tokio::time::sleep needs the time feature and an enabled time driver.