Rust Async: Tìm hiểu Future, Pin, và Waker trong async/await

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ứa Waker để 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
}

Biểu tượng ngôn ngữ lập trình Rust dùng cho phát triển hiệu năng cao

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ì:

  1. Self-referential structs: Future thường chứa con trỏ trỏ vào chính nó (ví dụ: trạng thái state machine)
  2. poll yêu cầu Pin<&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 Waker cho 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::sleep thay vì tokio::time::sleep sẽ 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

Sơ đồ trạng thái async/await trong Rust

Best practices

  1. Dùng async runtime thành thục: Tokio cho production, async-std cho đơn giản
  2. 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
  3. Hiểu về Send/Sync: Future cần Send để spawn trên multi-thread executor
  4. Dùng select!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.

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

Supabase: Open-source Firebase Alternative với PostgreSQL

Supabase là gì: Firebase Alternative mã nguồn mở dựa trên PostgreSQL Supabase là nền tảng phát triển ứng dụng mã nguồn mở (Apache 2.0, hơn 108K ⭐ trên GitHub) dựa…

Xem thêm

Drizzle ORM: TypeScript ORM hiện đại cho PostgreSQL và MySQL

Drizzle ORM là một ORM hiện đại dành cho TypeScript, hỗ trợ PostgreSQL, MySQL, SQLite và các database serverless như Turso, Neon, Cloudflare D1. Với kích thước chỉ ~7.4KB gzipped,…

Xem thêm

Git Worktree: Quản lý nhiều branch đồng thời trên cùng repo

Git Worktree là gì Bạn đang phát triển tính năng feature/A, rồi hotfix cấp bách từ branch main chen ngang. Thông thường bạn phải stash, checkout, sau đó quay lại…

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