Rust async pin Box Future hiểu đúng cơ chế an toàn bộ nhớ

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õ pinBox<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.

Sơ đồ cơ chế executor poll Future trong Rust async runtime
Executor liên tục poll Future cho đến khi sẵn sàng trả về Poll::Ready

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.

Minh họa giá trị được pin cố định trong bộ nhớ không bị move
Pin đảm bảo địa chỉ bộ nhớ không thay đổi cho self-referential struct
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

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Bevy Engine: Xây dựng game 2D/3D với Rust hiệu năng cao

Bevy là game engine mã nguồn mở được xây dựng bằng Rust, tập trung vào khả năng mở rộng (data-driven) và hiệu năng. Với kiến trúc ECS (Entity Component System)…

Xem thêm

SQLite FTS5 Full-Text Search: Tìm kiếm nội dung trong app không cần server

SQLite FTS5 (Full-Text Search version 5) là công cụ tìm kiếm toàn văn bản tích hợp sẵn trong SQLite, cho phép tìm kiếm nội dung văn bản nhanh chóng mà…

Xem thêm

Golang Concurrency Patterns: Worker Pool, Pipeline, Fan-out/Fan-in thực tế

Golang nổi tiếng với mô hình concurrency “share memory by communicating” thay vì “share memory by locking”. Nhưng pattern thông thường như worker pool, pipeline, fan-out/fan-in không tự động xuất…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất