Skip to content
DocsRust LearningexpertAsync/Await
Chapter 15 of 19·expert·10 min read

Async/Await

Bất Đồng Bộ

Asynchronous programming with Tokio

Hover or tap any paragraph to see Vietnamese translation

What is Async Programming

Async programming allows a program to perform multiple time-consuming operations concurrently without blocking the execution thread. Instead of creating an OS thread per task, async/await uses a lighter mechanism: tasks are multiplexed across a small number of threads.

Async is especially useful for I/O-bound tasks such as network requests, file reads/writes, or database queries — places where the program spends more time waiting than computing.

thread_vs_async.rs
1// Thread-based concurrency (one OS thread per task)2// Each thread has ~8MB stack - expensive for thousands of connections3use std::thread;45fn handle_thread() {6    let handle = thread::spawn(|| {7        // blocks the thread while waiting8        std::thread::sleep(std::time::Duration::from_secs(1));9        println!("Thread done");10    });11    handle.join().unwrap();12}1314// Async concurrency (tasks multiplexed on few threads)15// Each task is tiny in memory - cheap for thousands of connections16async fn handle_async() {17    // yields control while waiting instead of blocking18    tokio::time::sleep(std::time::Duration::from_secs(1)).await;19    println!("Async task done");20}

The Future Trait

A Future is a value representing an asynchronous computation that may not have completed yet. The Future trait has a single method poll(), which returns Poll::Ready(value) when done or Poll::Pending when more waiting is needed. You rarely implement Future directly — async fn does it for you.

future_trait.rs
1use std::future::Future;2use std::pin::Pin;3use std::task::{Context, Poll};4use std::time::{Duration, Instant};56// A simple future that completes after a duration7struct Delay {8    when: Instant,9}1011impl Future for Delay {12    type Output = ();

async fn and .await Syntax

Declaring a function with async fn turns it into a function that returns a Future. The .await operator inside an async fn suspends the function until that Future completes, yielding control to the runtime while waiting. You can only use .await inside an async context.

async_await_syntax.rs
1use tokio::time::{sleep, Duration};23// async fn returns impl Future<Output = String>4async fn fetch_user(id: u32) -> String {5    sleep(Duration::from_millis(10)).await;6    format!("User #{}", id)7}89async fn fetch_score(user_id: u32) -> u32 {10    sleep(Duration::from_millis(10)).await;11    user_id * 10012}
Tip
An async fn does not run immediately when you call it — it merely creates a Future. The Future only actually runs when you .await it or hand it to a runtime.

Async Runtimes: Tokio Overview

Rust has no built-in async runtime — you need an external crate. Tokio is the most popular runtime: it provides a thread pool, event loop, async I/O, timers, and concurrency primitives. The #[tokio::main] macro sets up the runtime and runs your async main function.

tokio_runtime.rs
1// Cargo.toml2// [dependencies]3// tokio = { version = "1", features = ["full"] }45// The #[tokio::main] macro expands to:6// fn main() {7//     tokio::runtime::Runtime::new().unwrap().block_on(async_main())8// }9#[tokio::main]10async fn main() {11    println!("Running on Tokio runtime");12    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

tokio::spawn for Concurrent Tasks

tokio::spawn creates a new async task that runs concurrently with the current task. Unlike thread::spawn, these spawned tasks are much lighter and are scheduled by the Tokio runtime. It returns a JoinHandle to await the result.

tokio_spawn.rs
1use tokio::time::{sleep, Duration};23async fn work(id: u32) -> u32 {4    sleep(Duration::from_millis(id as u64 * 10)).await;5    id * id6}78#[tokio::main]9async fn main() {10    // Spawn 5 tasks - all run concurrently11    let mut handles = vec![];12    for i in 1..=5 {

Async I/O: Reading Files and HTTP Requests

Tokio provides async versions of standard library I/O operations. tokio::fs handles async file I/O and crates like reqwest provide async HTTP clients. These operations yield the thread while waiting for I/O to complete.

async_io.rs
1use tokio::fs;2use tokio::io::{AsyncReadExt, AsyncWriteExt};34#[tokio::main]5async fn file_io_demo() -> Result<(), Box<dyn std::error::Error>> {6    // Async file write7    let mut file = fs::File::create("hello.txt").await?;8    file.write_all(b"Hello, async world!").await?;910    // Async file read11    let mut file = fs::File::open("hello.txt").await?;12    let mut contents = String::new();

The select! Macro

The tokio::select! macro awaits multiple Futures concurrently and executes the branch corresponding to whichever Future completes first. The remaining Futures are dropped. This is how you implement timeouts, race between data sources, or cancellation.

select_macro.rs
1use tokio::time::{sleep, Duration};2use tokio::sync::mpsc;34#[tokio::main]5async fn main() {6    // Race two operations: use whichever finishes first7    let result = tokio::select! {8        val = slow_operation() => format!("slow: {}", val),9        val = fast_operation() => format!("fast: {}", val),10    };11    println!("Winner: {}", result);12}

Streams

A Stream is the async version of an Iterator — a sequence of values produced over time. The tokio-stream crate provides the StreamExt trait with familiar combinators like map, filter, and collect. Streams are useful for processing data that arrives in chunks.

streams.rs
1// Cargo.toml: tokio-stream = "0.1"2use tokio_stream::{self as stream, StreamExt};34#[tokio::main]5async fn main() {6    // Create a stream from an iterator7    let mut s = stream::iter(vec![1, 2, 3, 4, 5]);89    while let Some(val) = s.next().await {10        println!("Got: {}", val);11    }12

Common Async Patterns

Common async patterns include: sequential execution, parallel execution with join!, retry logic, and semaphores for limiting concurrency. Knowing these patterns helps you write efficient and readable async code.

async_patterns.rs
1use tokio::sync::Semaphore;2use std::sync::Arc;34// Pattern 1: Sequential vs Concurrent5async fn sequential(ids: Vec<u32>) -> Vec<String> {6    let mut results = vec![];7    for id in ids {8        results.push(fetch(id).await);  // one at a time9    }10    results11}12

Pinning Basics

Pin<T> ensures that a value will not be moved in memory after being pinned. This matters for async/await because Futures can be self-referential, and moving them would corrupt internal pointers. Normally you do not need to handle Pin directly — the compiler and tokio::spawn handle this.

pinning_basics.rs
1use std::pin::Pin;23// Most of the time, the compiler pins futures for you.4// You only need Pin explicitly in advanced scenarios.56// Storing a future in a struct (needs Pin + Box)7use std::future::Future;89struct FutureHolder {10    // Pin<Box<dyn Future>> lets you store any future11    inner: Pin<Box<dyn Future<Output = String>>>,12}
Warning
If you see "future cannot be sent between threads safely", ensure all values held across a .await point implement Send. Non-Send local variables should not be alive at .await points.

Key Takeaways

Điểm Chính

  • Rust async functions return Futures that are lazy until awaitedHàm async trong Rust trả về Future, chỉ chạy khi được await
  • An async runtime like Tokio is required to execute futuresCần runtime async như Tokio để thực thi các future
  • Use tokio::spawn for concurrent task executionSử dụng tokio::spawn để thực thi tác vụ đồng thời
  • select! lets you race multiple futures and handle the first to completeselect! cho phép chạy đua nhiều future và xử lý cái hoàn thành trước

Practice

Test your understanding of this chapter

Quiz

What does calling an async fn without .await return?

Gọi một async fn mà không dùng .await trả về gì?

Quiz

When tokio::select! has multiple branches that become ready at the same time, which branch executes?

Khi tokio::select! có nhiều nhánh sẵn sàng cùng lúc, nhánh nào được thực thi?

True or False

Rust includes a built-in async runtime in its standard library, similar to how Go includes a built-in goroutine scheduler.

Rust có sẵn một async runtime tích hợp trong thư viện chuẩn, tương tự như Go có sẵn bộ lập lịch goroutine.

True or False

Pin<T> prevents a value from being moved in memory, which is necessary for self-referential async Futures to remain valid.

Pin<T> ngăn một giá trị bị di chuyển trong bộ nhớ, điều này cần thiết để các Future bất đồng bộ tự tham chiếu vẫn hợp lệ.

Code Challenge

Iterate an async stream by polling for the next item

Lặp qua một stream bất đồng bộ bằng cách lấy phần tử tiếp theo

let mut s = stream::iter(vec![1, 2, 3]);
while let Some(val) = s..await {
    println!("{}", val);
}

Chapter Complete!

Great job! Keep the momentum going.

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM