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.
Lập trình bất đồng bộ cho phép một chương trình thực hiện nhiều tác vụ tốn thời gian đồng thời mà không chặn luồng thực thi. Thay vì tạo một luồng OS cho mỗi tác vụ, async/await sử dụng một cơ chế nhẹ hơn: các tác vụ (tasks) được ghép kênh (multiplexed) trên một số ít luồng.
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.
Async đặc biệt hữu ích cho các tác vụ I/O-bound như yêu cầu mạng, đọc/ghi file, hoặc truy vấn cơ sở dữ liệu — những nơi mà chương trình dành nhiều thời gian chờ đợi hơn là thực thi.
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}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.
Một Future là một giá trị đại diện cho một phép tính bất đồng bộ có thể chưa hoàn thành. Trait Future có một phương thức duy nhất là poll(), trả về Poll::Ready(value) khi hoàn thành hoặc Poll::Pending khi cần chờ thêm. Bạn hiếm khi implement Future trực tiếp — async fn làm điều đó cho bạn.
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 = ();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.
Khai báo một hàm với async fn biến nó thành một hàm trả về một Future. Toán tử .await bên trong một async fn tạm dừng hàm cho đến khi Future đó hoàn thành, nhường quyền điều khiển cho runtime trong khi chờ đợi. Bạn chỉ có thể dùng .await bên trong async context.
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}Một async fn không chạy ngay khi bạn gọi nó — nó chỉ tạo ra một Future. Future chỉ thực sự chạy khi bạn .await nó hoặc chuyển nó cho một runtime.
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.
Rust không có async runtime tích hợp sẵn — bạn cần một crate bên ngoài. Tokio là runtime phổ biến nhất: nó cung cấp thread pool, event loop, async I/O, timer, và các nguyên thủy đồng thời. Macro #[tokio::main] thiết lập runtime và chạy hàm main bất đồng bộ.
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 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 tạo một tác vụ bất đồng bộ mới chạy đồng thời với tác vụ hiện tại. Khác với thread::spawn, các tác vụ được sinh ra này nhẹ hơn nhiều và được lập lịch bởi runtime Tokio. Hàm trả về một JoinHandle để chờ kết quả.
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 {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.
Tokio cung cấp các phiên bản bất đồng bộ của các thao tác I/O của thư viện chuẩn. tokio::fs cho file I/O và các crate như reqwest cung cấp HTTP client bất đồng bộ. Những thao tác này nhường luồng trong khi chờ I/O hoàn thành.
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 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.
Macro tokio::select! chờ nhiều Future đồng thời và thực thi nhánh ứng với Future hoàn thành đầu tiên. Các Future còn lại bị hủy. Đây là cách để implement timeout, race giữa nhiều nguồn dữ liệu, hoặc cancellation.
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}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.
Stream là phiên bản bất đồng bộ của Iterator — một chuỗi giá trị được tạo ra theo thời gian. Thư viện tokio-stream cung cấp StreamExt trait với nhiều combinator quen thuộc như map, filter, collect. Stream rất hữu ích cho việc xử lý dữ liệu đến theo từng mảnh.
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 }12Common 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.
Một số mẫu bất đồng bộ phổ biến bao gồm: chạy tuần tự, chạy song song với join!, retry logic, và semaphore để giới hạn đồng thời. Biết những mẫu này giúp bạn viết code async hiệu quả và dễ đọc.
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}12Pin<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.
Pin<T> đảm bảo rằng một giá trị sẽ không bị di chuyển trong bộ nhớ sau khi được ghim. Điều này quan trọng cho async/await vì các Future có thể tự tham chiếu (self-referential) và di chuyển chúng sẽ làm hỏng các con trỏ nội bộ. Thông thường bạn không cần xử lý Pin trực tiếp — compiler và tokio::spawn xử lý điều này.
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}Nếu bạn gặp lỗi 'future cannot be sent between threads safely', hãy đảm bảo rằng tất cả các giá trị được giữ qua một điểm .await đều implement Send. Biến cục bộ không Send không nên tồn tại khi .await.
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
What does calling an async fn without .await return?
Gọi một async fn mà không dùng .await trả về gì?
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?
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.
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ệ.
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); }