
Tại sao Rust async cần pin và Box<dyn Future>?
Rust async/await là một trong những tính năng mạnh mẽ nhất của ngôn ngữ, nhưng để làm chủ nó, bạn cần hiểu rõ pin và Box<dyn Future>. Đây là hai khái hình cốt lõi giúp Rust đảm bảo an toàn bộ nhớ mà không cần garbage collector, đồng thời cho phép viết code bất đồng bộ linh hoạt.
Một Future trong Rust là giá trị đại diện cho một phép tính bất đồng bộ chưa hoàn thành. Khác với JavaScript hay Python, Rust không chạy Future tự động — nó chỉ tiến hành khi executor gọi poll(). Điều này mang lại hiệu năng tuyệt vời nhưng cũng đặt ra yêu cầu nghiêm ngặt về ownership và lifetime.

Cơ chế pin: Giữ giá trị cố định trong bộ nhớ
Khi một Future bắt đầu thực thi, executor sẽ lưu trữ state của nó. Nếu Future được di chuyển (move) trong bộ nhớ, con trỏ nội bộ sẽ bị lỗi — vì Rust không cho phép con trỏ tự do linh hoạt như C++. Đây chính là lý do cần pin.
Pin đảm bảo giá trị được “đóng cọc” tại một vị trí bộ nhớ cụ thể. Khi bạn Pin<Box>, bạn hứa với compiler rằng T sẽ không bao giờ bị move. Từ đó, Future có thể lưu trữ con trỏ tự tham chiếu đến chính nó một cách an toàn.

use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
fn poll_future<F>(future: Pin<&mut F>) -> Poll<F::Output>
where
F: Future,
{
let waker = /* ... */;
let mut cx = Context::from_waker(&waker);
future.poll(&mut cx)
}
Box<dyn Future> khi nào dùng?
Box<dyn Future<Output = T> + Send + 'static> là kiểu phổ biến nhất trong async Rust thực tế. Nó cho phép bạn trả về các Future với kiểu khác nhau từ cùng một hàm mà không cần generic. Ví dụ, khi match trên nhiều kiểu Future trong một async fn, trình compiler cần một kiểu duy nhất — và Box<dyn Future> đáp ứng điều đó.
Tuy nhiên, có chi phí: mỗi Box cấp phát trên heap và thêm một lớp gián tiếp (indirection) khi truy cập. Trong các đoạn code nhạy cảm hiệu năng, bạn có thể thay thế bằng enum hoặc generic để tránh allocation.
So sánh Pin::new vs Box::pin
Hai cách chính để tạo giá trị pinned:
- Box::pin(value): Cấp phát value lên heap, trả về
Pin<Box<T>>. Đây là cách phổ biến nhất khi bạn cần Future có thể trả về từ hàm. - Pin::new(&mut value): Tạo
Pin<&mut T>từ reference. Phù hợp trong async fn thông thường vì executor đã đảm bảo local variables không bị move.
Ví dụ thực tế: async fn trả về Box<dyn Future>
Dưới đây là pattern thường gặp khi viết thư viện hoặc trait có nhiều implement:
trait ApiClient {
fn fetch_user(&self, id: u64) -> Box<dyn Future<Output = Result<User, Error>> + Send + '_>;
}
struct RestClient { base_url: String }
impl ApiClient for RestClient {
fn fetch_user(&self, id: u64) -> Box<dyn Future<Output = Result<User, Error>> + Send + '_> {
Box::pin(async move {
let url = format!("{}/users/{}", self.base_url, id);
let resp = reqwest::get(&url).await?;
let user = resp.json::<User>().await?;
Ok(user)
})
}
}
Lỗi thường gặp và cách khắc phục
Lỗi phổ biến nhất là compiler báo the trait Future cannot be made into an object hoặc cannot move out of a pinned value. Nguyên nhân thường là bạn cố gắng trả về impl Future trong trait, hoặc move giá trị đã bị pin. Giải pháp là dùng Box::pin và đảm bảo Send + 'static khi cần.
Khi nào dùng enum thay vì Box<dyn Future>
Nếu bạn biết trước tất cả các kiểu Future có thể xuất hiện, hãy dùng enum thay vì Box. Enum không cần heap allocation, không có gián tiếp và compiler có thể inline các arm. Ví dụ, thay vì Box<dyn Future<Output = ...>>, bạn có thể định nghĩa:
enum UserFuture {
Cached(User),
Fetch(Box<dyn Future<Output = Result<User, Error>> + Send>),
}
Kết luận
Pin và Box<dyn Future> là công cụ cốt lõi để viết async Rust linh hoạt. Pin đảm bảo an toàn bộ nhớ cho các giá trị tự tham chiếu, trong khi Box<dyn Future> cho phép trả về các kiểu khác nhau một cách đồng nhất. Hiểu rõ khi nào dùng cái nào giúp bạn tránh chi phí allocation không cần thiết và viết code async Rust sạch hơn.
Xem thêm: Rust Pin Documentation, Asynchronous Programming in Rust – Pinning
