
Rust Async: Tìm hiểu Future, Pin, và Waker trong async/await
Rust async/await là một trong những tính năng mạnh mẽ nhất của ngôn ngữ, cho phép viết code không đồng bộ (non-blocking) với cú pháp trông như code đồng bộ. Tuy nhiên, bên dưới bề mặt, cơ chế này dựa trên ba khái niệm cốt lõi: Future, Pin, và Waker. Hiểu rõ chúng giúp bạn viết code hiệu quả hơn và tránh các bẫy phổ biến. Bạn có thể tham khảo tài liệu chính thức về Rust async tại docs.rust-lang.org/std/future.
Future: Trái tim của async Rust
Future trong Rust là một trait đại diện cho một phép tính chưa hoàn thành có thể trả về giá trị trong tương lai. Khác với JavaScript Promise hay C# Task, Rust Future là lazy — nó không làm gì cho đến khi được poll (truy vấn).
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Phương thức poll nhận hai tham số quan trọng:
Pin<&mut Self>: Đảm bảo Future không bị di chuyển trong bộ nhớ&mut Context: ChứaWakerđể báo hiệu khi Future sẵn sàng
Kết quả trả về là enum Poll:
enum Poll<T> {
Ready(T), // Phép tính hoàn tất
Pending, // Chưa xong, sẽ được đánh thức sau
}

Tại sao cần Pin?
Pin<P> là một smart pointer đảm bảo dữ liệu nó trỏ đến không bao giờ bị di chuyển trong bộ nhớ. Điều này bắt buộc vì:
- Self-referential structs: Future thường chứa con trỏ trỏ vào chính nó (ví dụ: trạng thái state machine)
pollyêu cầuPin<&mut Self>: Compiler bắt buộc Future không được move sau khi poll lần đầu
Ví dụ trạng thái của một async function:
// async fn foo() { let x = 1; bar().await; baz(x).await; }
// Biên dịch thành state machine tương tự:
enum FooState {
Start { x: i32 }, // Trước await đầu tiên
WaitingBar { x: i32 }, // Đang chờ bar()
WaitingBaz { result: i32 }, // Đang chờ baz()
Done,
}
Nếu struct này bị move trong bộ nhớ, các con trỏ nội bộ sẽ trỏ sai địa chỉ. Pin ngăn chặn điều này bằng cách không implement Unpin cho các type có self-reference.
Waker: Cơ chế thức dậy
Waker là một handle cho phép executor (runtime như Tokio, async-std) biết khi nào nên poll lại một Future. Khi một Future trả về Poll::Pending, nó lưu Waker từ Context và gọi wake() khi dữ liệu sẵn sàng.
struct Context<'a> {
waker: &'a Waker,
}
impl Waker {
fn wake(self); // Tiêu thụ Waker, đánh thức task
fn wake_by_ref(&self); // Chỉ tham chiếu, không tiêu thụ
}
Cơ chế này tránh busy polling — executor chỉ poll Future khi có sự kiện thực sự xảy ra (I/O ready, timer expired, channel có data).
Executor: Người điều phối
Executor là runtime chịu trách nhiệm:
- Quản lý hàng đợi task
- Poll các Future khi được đánh thức
- Cung cấp
Wakercho từng task
Khi bạn gọi spawn hoặc block_on, executor tạo một task mới, gán Waker trỏ về chính task đó, và bắt đầu poll Future gốc.
Các bẫy thường gặp
- Quên .await: Future không chạy nếu không được poll
- Block trong async context: Dùng
std::thread::sleepthay vìtokio::time::sleepsẽ block cả thread executor - Move Future sau khi poll: Vi phạm Pin, dẫn đến undefined behavior
- Quên wake: Future trả về Pending nhưng không bao giờ được wake — task treo vô hạn

Best practices
- Dùng async runtime thành thục: Tokio cho production, async-std cho đơn giản
- Tránh Pin thủ công: Hầu hết code không cần tương tác trực tiếp với Pin
- Hiểu về Send/Sync: Future cần
Sendđể spawn trên multi-thread executor - Dùng
select!vàjoin!đúng cách: Để chạy nhiều Future song song
Kết luận
Hiểu Future, Pin, và Waker không chỉ giúp bạn debug tốt hơn mà còn cho phép tận dụng tối đa sức mạnh của async Rust. Rust chọn cách tiếp cận zero-cost abstraction — async/await compile xuống state machine không có overhead runtime, nhưng đòi hỏi người lập trình hiểu rõ cơ chế bên dưới. Với kiến thức này, bạn có thể viết các hệ thống hiệu năng cao, an toàn bộ nhớ và có thể mở rộng mà không lo rơi vào các bẫy tinh vi của asynchronous programming.
