Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Viết được
async fnđơn giản với cú pháp đúng và hiểu khác biệt so vớifnthường ở chỗ nào. - Hiểu bản chất của
async fn: gọi nó không chạy body — nó trả về mộtFuture(state machine) mà runtime phải drive. - Đọc được desugaring
async fn foo() -> T⇔fn foo() -> impl Future<Output = T>và biết khi nào hai dạng tương đương, khi nào lệch về lifetime. - Biết bắt buộc dùng
.awaittrong async context để lấy giá trị thực sự, và không thể.awaittrong sync function. - Nhận diện được warning "unused
Futurethat must be used" khi quên.await— và biết đây là vìFuturecó attribute#[must_use]. - Khai báo được
async fntrongimplblock của struct và trongtrait(nhờ AFIT ổn định từ Rust 1.75). - Biết hạn chế hiện tại của AFIT (chưa hỗ trợ trait object
dyn Traittrực tiếp) và workaround#[async_trait]khi cần dynamic dispatch.
async fn — Cú Pháp Cơ Bản
Cú pháp đơn giản đến mức gây hiểu lầm: chỉ thêm keyword async trước fn:
// Sync function thường
fn foo() -> i32 {
42
}
// Async function — chỉ khác chữ `async`
async fn bar() -> i32 {
42
}
Mọi thứ còn lại — tên hàm, parameter, return type, body — viết y hệt. Body có thể chứa code Rust bình thường: tính toán, gọi function khác, dùng let, if, match, ?. Có thể nhận parameter, có thể trả về Result, có thể là pub:
pub async fn fetch_user(id: u64) -> Result<String, std::io::Error> {
// pretend ta đọc file hoặc gọi HTTP ở đây
Ok(format!("user {id}"))
}
Một điểm nhỏ nhưng quan trọng: fn main() mặc định không được async — entry point của binary là sync. Muốn có async fn main, bạn phải dùng macro của runtime, ví dụ #[tokio::main] ở các bài sau. Còn lúc này, ta sẽ viết các async fn nhỏ và chạy chúng từ một sync main qua runtime.
Bản Chất: async fn Trả Về Future
Đây là điểm hoàn toàn khác với sync function và là chỗ nhiều người mới vấp. Nhìn signature async fn bar() -> i32, bạn rất dễ tưởng bar() trả về i32. Sự thật: gọi bar() không chạy body và không trả về i32. Nó trả về một giá trị mới thuộc một kiểu ẩn danh implement trait Future<Output = i32> — một state machine mà compiler tự sinh, chứa toàn bộ code body của bar đóng băng lại, chờ ai đó drive nó.
async fn bar() -> i32 { 42 }
fn main() {
let fut = bar(); // KHÔNG in gì cả; body của bar() chưa chạy.
// `fut` có kiểu `impl Future<Output = i32>` — chưa phải i32.
drop(fut); // drop future = vứt state machine, không có side effect.
}
Đặc tính này gọi là lazy: future trong Rust không chạy cho đến khi được poll. Trái ngược với JavaScript Promise hay C# Task — eager bắt đầu chạy ngay khi tạo. Lazy có ưu thế: cho phép cancel "miễn phí" (drop future = huỷ), không tốn resource cho future không được dùng, và compiler có thể fuse nhiều future thành state machine duy nhất.
Hệ quả thực dụng: chỉ tạo future không đủ để code chạy. Phải có .await hoặc trao future cho một executor (như tokio::spawn). Đây là khác biệt nền tảng cần ghi nhớ trước khi đi tiếp.
Desugaring: fn -> impl Future<Output = T>
Compiler thực chất viết lại async fn thành một sync fn trả về impl Future. Hai khai báo dưới là tương đương về mặt ngữ nghĩa:
// Dạng async fn (cú pháp đường)
async fn fetch(id: u64) -> String {
format!("user {id}")
}
// Dạng desugar tương đương
fn fetch(id: u64) -> impl std::future::Future<Output = String> {
async move {
format!("user {id}")
}
}
Hai cách viết này có cùng signature ở góc nhìn của caller — đều trả về thứ implement Future<Output = String>, đều cần .await để lấy String ra. Trong code thực, gần như ai cũng dùng dạng async fn vì gọn hơn; dạng desugar chỉ xuất hiện khi bạn cần kiểm soát chi tiết hoặc khi đọc tài liệu của compiler.
Một số trường hợp không hoàn toàn tương đương là khi có lifetime dính vào parameter reference:
// async fn — lifetime của tham chiếu tự động bind vào future trả về
async fn read_first(buf: &[u8]) -> u8 {
buf[0]
}
// Desugar đầy đủ (Rust ngầm hiểu)
fn read_first<'a>(buf: &'a [u8]) -> impl std::future::Future<Output = u8> + 'a {
async move { buf[0] }
}
Điểm chốt: future trả về vẫn còn giữ tham chiếu &[u8] cho đến khi nó .await xong, nên future phải có lifetime 'a. Với async fn, compiler tự ngầm thêm; với dạng desugar viết tay, bạn phải khai báo rõ + 'a. Đây là lý do bạn sẽ thấy + 'a nhan nhản trong signature lib async.
Gọi async fn Phải .await
Để biến impl Future<Output = T> thành T, bạn dùng toán tử hậu tố .await. Toán tử này chỉ dùng được trong async context — nghĩa là bên trong một async fn hoặc một async { ... } block khác.
async fn bar() -> i32 { 42 }
async fn caller() -> i32 {
let x: i32 = bar().await; // .await chạy bar() và chờ kết quả
x + 1
}
.await nói với runtime: "tôi cần kết quả của future này; nếu nó chưa sẵn sàng, hãy tạm dừng tôi và làm việc khác". Khi future ready, runtime đánh thức và tiếp tục đoạn code phía sau. Đây là cách Rust làm cooperative concurrency trong một thread duy nhất, không cần thread mới cho mỗi tác vụ.
Trong sync context, .await là compile error:
fn main() {
let x = bar().await; // error[E0728]: `await` is only allowed inside `async` functions and blocks
}
Để chạy một future từ sync main, bạn cần một runtime — thường là tokio. Mẫu phổ biến nhất:
// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
let x = bar().await;
println!("{x}"); // 42
}
Macro #[tokio::main] tự sinh sync main bên trong, tạo runtime, rồi gọi runtime.block_on(async_body). Chi tiết runtime sẽ ở các bài 246+, ở đây chỉ cần biết "muốn chạy async, phải có runtime".
Lỗi Phổ Biến: Quên .await
Vì cú pháp async fn trông giống sync fn, người mới rất hay viết:
async fn save(name: &str) {
// pretend ghi DB
}
#[tokio::main]
async fn main() {
save("an"); // QUÊN .await — không ghi gì cả!
}
Code biên dịch được, nhưng save không bao giờ chạy. Bạn chỉ tạo ra một future rồi vứt đi ngay (lazy + drop = no-op). Đây là class bug rất khó debug nếu không quen.
May mắn, trait Future trong stdlib có attribute #[must_use = "futures do nothing unless you `.await` or poll them"]. Compiler sẽ warn ngay lập tức:
warning: unused implementer of `Future` that must be used
--> src/main.rs:7:5
|
7 | save("an");
| ^^^^^^^^^^
|
= note: futures do nothing unless you `.await` or poll them
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
7 | let _ = save("an");
| +++++++
Đừng silence warning này bằng let _ = save("an"); như compiler đề xuất — đó là cách tắt warning chứ không phải fix. Fix đúng là thêm .await (chờ chạy xong) hoặc đẩy vào executor (tokio::spawn(save("an")) để chạy concurrent). Tip: bật #![deny(unused_must_use)] ở crate root để biến warning này thành compile error — code async sẽ an toàn hơn nhiều.
Một biến thể khác cũng hay gặp: gọi .await rồi nhưng bên trong sync closure (ví dụ .map(|x| async_op(x).await)) — closure đó không phải async block, cũng báo lỗi. Sửa thành .then(|x| async_op(x)) hoặc dùng async closure.
async fn Trong impl Block Và Trait
async fn hoạt động bình thường trong impl block của struct. Cú pháp giống hệt:
struct UserRepo {
base_url: String,
}
impl UserRepo {
async fn find(&self, id: u64) -> Option<String> {
// pretend HTTP GET {base_url}/users/{id}
Some(format!("user {id} @ {}", self.base_url))
}
}
#[tokio::main]
async fn main() {
let repo = UserRepo { base_url: "http://api".into() };
if let Some(u) = repo.find(7).await {
println!("{u}");
}
}
Lưu ý &self: future trả về sẽ giữ tham chiếu &UserRepo đến khi .await xong — repo không được drop trong khoảng đó. Borrow checker tự xử lý.
Trong trait, mọi chuyện từng rắc rối hơn rất nhiều — trước Rust 1.75, viết async fn trong trait không compile, phải dùng macro #[async_trait] của crate async-trait để rewrite thành fn -> Pin<Box<dyn Future>>. Từ Rust 1.75 (12/2023), tính năng AFIT (async fn in traits) đã ổn định, bạn viết thẳng được:
trait Repo {
async fn find(&self, id: u64) -> Option<String>;
}
struct InMemRepo;
impl Repo for InMemRepo {
async fn find(&self, id: u64) -> Option<String> {
Some(format!("in-mem user {id}"))
}
}
async fn use_repo<R: Repo>(repo: &R) {
if let Some(u) = repo.find(42).await {
println!("{u}");
}
}
Hạn chế: AFIT hiện chưa hỗ trợ trực tiếp dyn Trait (trait object) — không thể có Box<dyn Repo> với async fn trong trait. Lý do: future trả về có kiểu ẩn danh, không cố định kích thước, không gắn vào vtable thẳng được. Hai workaround chính:
- Generic bound (
fn use_repo<R: Repo>(repo: &R)) như ví dụ trên — static dispatch, nhanh, không cầndyn. - Khi bắt buộc phải có
dyn Repo(lưu nhiều implementation khác nhau trong mộtVec), vẫn dùng macro#[async_trait::async_trait]như trước — nó rewrite thànhfn -> Pin<Box<dyn Future + Send>>tương thích vớidyn.
Tin vui: cả hai cách đều phổ biến và đều idiomatic ở thời điểm 2026. Crate async-trait vẫn được maintain tốt và sẽ tồn tại cho đến khi RFC dyn-compatible async-fn-in-trait được hoàn thiện.
Tổng Kết
async fn: chỉ thêm keywordasynctrướcfnlà xong. Body viết code Rust bình thường, có?,match,let... như sync.- Bản chất: gọi
async fnkhông chạy body. Nó trả về một giá trị thuộc kiểu ẩn danh implementFuture<Output = T>— state machine compiler-generated, lazy, chờ ai đó drive. - Desugaring:
async fn foo() -> T⇔fn foo() -> impl Future<Output = T>. Khi parameter có reference, lifetime tự động bind vào future trả về (+ 'a). - Phải
.awaitđể lấy giá trị thực sự..awaitchỉ dùng được trong async context (async fn hoặc async block). Sync context cần runtime (#[tokio::main],block_on). - Quên
.await: body không chạy. Compiler warn nhờ#[must_use]trênFuture. Tip: bật#![deny(unused_must_use)]để biến warning thành error. - Trong
implblock: cú pháp y hệt,&self/&mut selfhoạt động bình thường, lifetime tự xử lý. - Trong
trait: từ Rust 1.75 (12/2023), AFIT ổn định — viết thẳngasync fntrong trait không cần macro. Hạn chế: chưa hỗ trợdyn Trait; cần dynamic dispatch thì dùng#[async_trait::async_trait]như cũ. - Lazy semantics: future Rust khác Promise JS/Task C# — không tự chạy khi tạo. Ưu điểm: cancel miễn phí (drop = huỷ), không waste resource, compiler optimize tốt hơn.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết
async fn add(a: i32, b: i32) -> i32trả về tổng. Sau đó viếtasync fn main_logic()gọiadd(2, 3)và in kết quả. Vì sao gọi không có.awaitsẽ không in gì? - Cho
async fn ping() -> bool { true }. Viết dạng desugar tương đương dùngfn -> impl Future<Output = bool>vàasync { ... }block. - Đoạn code
let x = ping().await;đặt trongfn main()(sync) báo lỗi gì? Cách sửa ngắn gọn nhất? - Vì sao Rust chọn future lazy (không tự chạy) thay vì eager như JavaScript Promise? Nêu ít nhất hai lợi ích.
- Trước Rust 1.75, làm thế nào để có
async fntrong trait? Sau Rust 1.75 thì khác gì, và hạn chế gì còn lại? - Viết một
trait Cachecó methodasync fn get(&self, key: &str) -> Option<String>, sau đó implement cho structMemCachetrả vềSome(key.to_string()).
Đáp án
async fn add(a: i32, b: i32) -> i32 { a + b }.async fn main_logic() { let s = add(2, 3).await; println!("{s}"); }. Gọi không.awaitchỉ tạo future (state machine) rồi drop ngay; body củaaddchưa chạy lần nào, không có side effect nào xảy ra — Rust future là lazy.fn ping() -> impl std::future::Future<Output = bool> { async { true } }. Tương đương ngữ nghĩa vớiasync fn ping() -> bool { true }.- Báo lỗi
error[E0728]: \`await\` is only allowed inside \`async\` functions and blocks. Sửa ngắn nhất: thêm#[tokio::main]và đổifn mainthànhasync fn main(cần dependencytokiovới featurefullhoặcrt-multi-thread + macros). - (a) Cancel miễn phí: drop future = huỷ tác vụ, không cần cơ chế cancel token phức tạp. (b) Không waste resource: future chưa
.awaitkhông tốn gì. (c) Compiler có thể fuse nhiều future thành một state machine duy nhất, tối ưu allocation. (d) Composability: dễ build combinator nhưselect!,join!trên top. - Trước 1.75: phải dùng macro
#[async_trait::async_trait]của crateasync-trait— nó rewriteasync fnthànhfn -> Pin<Box<dyn Future + Send + 'a>>. Từ 1.75: viết thẳngasync fntrong trait (AFIT). Hạn chế còn lại: chưa hỗ trợ trực tiếpdyn Trait(trait object); muốnBox<dyn MyTrait>vẫn cần#[async_trait]. trait Cache { async fn get(&self, key: &str) -> Option<String>; } struct MemCache; impl Cache for MemCache { async fn get(&self, key: &str) -> Option<String> { Some(key.to_string()) } }. Dùng được trong generic contextfn run<C: Cache>(c: &C); muốnBox<dyn Cache>cần#[async_trait].
Bài Tiếp Theo
Bài 242: async Block — async { ... } — bài tiếp khám phá async { ... } block: cách tạo một future inline ngay tại chỗ mà không cần khai báo function riêng, sự khác biệt giữa async block và async move block (capture by reference vs by value), cách kết hợp nhiều async block trong cùng scope, và use case xây future tùy biến hoặc tạo future ngắn để pass vào tokio::spawn. Sau đó bài 243 sẽ mổ xẻ chính trait Future: poll, Pin, Waker — bản chất state machine mà compiler sinh ra cho mọi async fn.
