ข้ามไปยังเนื้อหา

Async Introduction

syntax async/.await ของ Rust ให้คุณเขียนโค้ด non-blocking ที่ดูเหมือนโค้ด sequential ธรรมดา ภายใต้ประทุน async fn ส่งคืน Future — ค่าที่แสดงถึงการคำนวณที่อาจยังไม่เสร็จสมบูรณ์

// @skip-verify crate: tokio
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(10)).await;
format!("data for id={}", id)
}
#[tokio::main]
async fn main() {
let result = fetch_data(1).await;
println!("{}", result);
}

คำสั่ง .await ระงับ async function ปัจจุบันจนกว่า Future ที่ถูกนำไปใช้จะ resolve ขณะที่ระงับอยู่ executor (tokio) สามารถรัน task อื่น ๆ ได้

Future<Output = T> เป็น trait ที่แสดงถึงค่าประเภท T ที่จะพร้อมในบางจุดในอนาคต การเรียก async fn ไม่ได้รัน body ทันที — แต่ส่งคืน Future กลับมา body จะรันเฉพาะตอนคุณ .await (หรือ poll ผ่าน executor)

// @skip-verify crate: tokio
// This does nothing yet — just creates a Future
let fut = fetch_data(42);
// This runs the body and blocks until complete
let result = fut.await;

การประเมินแบบ lazy นี้คือเหตุผลที่ async ของ Rust มีต้นทุนเป็นศูนย์: future ที่ไม่เคย await จะไม่รันเลย

tokio เป็น external crate แต่ trait Future, Pin และ Waker ทั้งหมดอยู่ใน std — คุณสร้าง executor ที่ง่ายที่สุดในโลกได้โดยไม่ต้องใช้อะไรอื่นเลย แล้วดู laziness เกิดขึ้นด้วย println! ของคุณเองแทนที่จะเชื่อลอย ๆ:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// A waker that does nothing when woken. Good enough for a future that
// finishes the first time it is polled — no real async I/O involved.
fn noop_waker() -> Waker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
raw_waker()
}
fn raw_waker() -> RawWaker {
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(std::ptr::null(), &VTABLE)
}
unsafe { Waker::from_raw(raw_waker()) }
}
// The smallest possible executor: poll the future in a loop until it is Ready.
fn block_on<F: Future>(mut future: F) -> F::Output {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
// SAFETY: future is a local variable that is never moved again after
// this point, so pinning it on the stack is sound.
let mut future = unsafe { Pin::new_unchecked(&mut future) };
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => continue, // no real waker source, so just poll again
}
}
}
async fn greet(name: &str) -> String {
println!("greet body is running for {name}");
format!("Hello, {name}!")
}
fn main() {
println!("before calling greet()");
let fut = greet("Rust"); // nothing has run yet — this only builds a Future
println!("after calling greet(), before polling");
let message = block_on(fut); // this is what actually runs the body
println!("{message}");
}
before calling greet()
after calling greet(), before polling
greet body is running for Rust
Hello, Rust!

สังเกตลำดับ: "greet body is running" พิมพ์ หลังจาก "before polling" ไม่ใช่ตอนที่เรียก greet("Rust") การเรียก async fn แค่สร้างค่าที่อธิบายงานที่ต้องทำ — body ไม่รันสักบรรทัดจนกว่าจะมีอะไรมา poll มัน ซึ่งนั่นคือสิ่งที่ block_on (และ tokio ที่ใช้ waker จริงที่มีประสิทธิภาพแทนตัว no-op นี้) ทำให้คุณ

flowchart TD
  A["async fn call() returns a Future — nothing has run yet"] --> B["executor calls poll()"]
  B --> C{"body reaches an .await on an unfinished future?"}
  C -->|"no — runs to completion"| D["Poll::Ready(value)"]
  C -->|"yes"| E["Poll::Pending — state machine suspends here"]
  E --> F["waker fires when the awaited future can progress"]
  F --> B
สิ่งที่ .await ถูกแตกออกเป็น: loop ของการ poll state machine

แต่ละจุด .await คือจุดที่คอมไพเลอร์สามารถระงับ state machine ที่สร้างขึ้นและคืนค่า Poll::Pending ได้ หน้าที่ของ executor (ตัวจริงของ tokio หรือ block_on ของเล่นในบทนี้) คือเรียก poll ซ้ำทุกครั้งที่มีโอกาสที่ future จะคืบหน้าต่อได้

เพื่อรัน async task หลายตัวในเวลาเดียวกัน ใช้ tokio::join! ซึ่ง drive future ทั้งหมดพร้อมกันบน thread เดียว (หรือข้าม worker thread ขึ้นอยู่กับ runtime configuration)

// @skip-verify crate: tokio
use tokio::time::{sleep, Duration};
async fn task(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
format!("{} done", name)
}
#[tokio::main]
async fn main() {
let (a, b, c) = tokio::join!(
task("alpha", 30),
task("beta", 20),
task("gamma", 10),
);
println!("{}", a);
println!("{}", b);
println!("{}", c);
}

task ทั้งสามเริ่มทันที tokio::join! รอจนทุกตัวเสร็จและส่งคืนผลลัพธ์ตามลำดับ

tokio::spawn เปิดตัว task บน tokio thread pool แล้วส่งคืน JoinHandle คล้ายกับ std::thread::spawn

// @skip-verify crate: tokio
use tokio::task;
#[tokio::main]
async fn main() {
let handle = task::spawn(async {
// runs concurrently on the tokio thread pool
42_u32
});
let result = handle.await.unwrap();
println!("Spawned task returned: {}", result);
}

tokio::spawn (เหมือน std::thread::spawn) อาจรัน future ของมันบน worker thread ที่ต่างจาก thread ที่สร้างมัน ดังนั้น future — และทุกอย่างที่มันถืออยู่ข้าม .await — ต้องเป็น Send การถือ Rc<T> ข้าม .await ทำให้กฎนี้พัง:

// @skip-verify crate: tokio
use std::rc::Rc;
async fn uses_rc() {
let shared = Rc::new(42);
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
println!("{shared}");
}
#[tokio::main]
async fn main() {
tokio::spawn(uses_rc()).await.unwrap();
}
error: future cannot be sent between threads safely
--> src/main.rs:11:18
|
11 | tokio::spawn(uses_rc()).await.unwrap();
| ^^^^^^^^^ future returned by `uses_rc` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<i32>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:5:61
|
4 | let shared = Rc::new(42);
| ------ has type `Rc<i32>` which is not `Send`
5 | tokio::time::sleep(std::time::Duration::from_millis(1)).await;
| ^^^^^ await occurs here, with `shared` maybe used later
note: required by a bound in `tokio::spawn`
--> tokio-1.53.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`

สังเกตคำพูดของ diagnostic เอง — Rc<i32> “used across an await” หมายความว่ามันยังมีชีวิตอยู่ทั้งสองฝั่งของจุดระงับ เปลี่ยน Rc::new เป็น Arc::new แล้ว future จะกลับมาเป็น Send อีกครั้ง เหมือนในบท threads เป๊ะ นี่คือ Send bound เดียวกับ std::thread::spawn เพียงแต่ถูกบังคับใช้กับ state ที่ struct ที่สร้างจาก async fn ที่ถูกระงับต้องพกไว้ระหว่างการ poll แต่ละครั้ง แทนที่จะเป็นสิ่งที่ closure capture

การถือ Mutex แบบ synchronous ข้าม .await ดูเหมือนไม่มีปัญหาสำหรับ borrow checker แต่เป็นความเสี่ยงด้าน liveness จริง: executor สามารถระงับ task ของคุณกลาง .await ขณะที่ยังถือ guard อยู่ และถ้า executor นั้นบังเอิญรัน task อื่นบน thread เดียวกัน ที่ต้องการ lock นั้นด้วย คุณจะได้ deadlock ที่ type system มองไม่เห็น (บั๊กคลาสเดียวกับ deadlock สองล็อคในบท threads แต่คราวนี้มีแค่ lock เดียว)

// @skip-verify crate: tokio
use std::sync::Mutex;
async fn bad_pattern(shared: &Mutex<i32>) {
let guard = shared.lock().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1)).await; // guard is still held here
println!("{}", *guard);
}
warning: this `MutexGuard` is held across an await point
--> src/main.rs:4:9
|
4 | let guard = shared.lock().unwrap();
| ^^^^^
|
= help: consider using an async-aware `Mutex` type or ensuring the `MutexGuard` is dropped before calling `await`
= note: `#[warn(clippy::await_holding_lock)]` on by default

ทางแก้: drop guard ก่อน .await (คำนวณสิ่งที่ต้องการแล้วปล่อย lock) หรือใช้ lock แบบ async-aware อย่าง tokio::sync::Mutex ถ้าคุณต้องถือมันข้ามจุดระงับจริง ๆ

std::thread::sleep ภายใน async fn บล็อก OS thread ทั้งตัวที่ executor ใช้อยู่ — รวมถึงทุก task อื่นที่ scheduled บน thread นั้น — เพราะมันเป็นการเรียกแบบ synchronous ที่ไม่มี .await เลย executor จึงไม่มีโอกาสรันอะไรอื่นระหว่างที่รอ:

// @skip-verify crate: tokio
async fn bad_delay() {
std::thread::sleep(std::time::Duration::from_millis(100)); // blocks the whole executor thread
println!("done");
}
async fn good_delay() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; // yields control back to the executor
println!("done");
}

tokio::time::sleep(...).await คืนการควบคุมกลับให้ executor ตลอดช่วงเวลานั้น ทำให้ task อื่นรันบน thread นั้นได้ระหว่างนั้น ถ้าความล่าช้าเป็นงาน CPU-bound ที่หลีกเลี่ยงไม่ได้จริง ๆ (ไม่ใช่แค่ sleep) เครื่องมือที่ถูกต้องคือ tokio::task::spawn_blocking ซึ่งย้ายงานที่บล็อกไปยัง thread pool เฉพาะที่สร้างไว้เพื่อจุดประสงค์นี้ ทำให้ thread ของ async executor ว่างอยู่

ตั้งแต่ Rust 1.75 คุณเขียน async fn ตรง ๆ ใน trait definition ได้เลย — ไม่ต้องใช้ crate async-trait สำหรับกรณีทั่วไป:

trait Greeter {
async fn greet(&self, name: &str) -> String;
}
struct English;
impl Greeter for English {
async fn greet(&self, name: &str) -> String {
format!("Hello, {name}!")
}
}

โค้ดนี้ compile และรันได้บน std ธรรมดา — ไม่ต้องใช้ tokio เลยด้วยซ้ำสำหรับจุดเรียกที่แค่ .await มันตรง ๆ ข้อจำกัดจริงข้อเดียว: คุณยังทำ trait ที่มี async fn ให้เป็น dyn Trait object ไม่ได้ในวันนี้ (แต่ละ implementation มี future เป็น type นิรนามที่แตกต่างและขนาดต่างกัน ส่วน dyn ต้องการขนาดคงที่หนึ่งเดียว) — สำหรับ trait object คุณยังต้องใช้ desugaring แบบ Box<dyn Future<...>> ของ crate async-trait หรือเขียน signature -> impl Future<Output = ...> เอง

เกณฑ์OS Threads (std::thread)Async (tokio)
งาน CPU-boundแนะนำใช้ spawn_blocking
งาน I/O-boundสิ้นเปลือง (thread ถูกบล็อก)แนะนำ
จำนวน task พร้อมกันหลายร้อย (จำกัดโดย OS)หลายล้าน (น้ำหนักเบา)
ขนาด stack~8 MB ต่อ thread~ไม่กี่ KB ต่อ task
ต้องการ external crateไม่ใช่ (tokio)
Terminal window
cargo add tokio --features full

หรือใน Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

จากนั้น annotate entry point ของคุณ:

// @skip-verify crate: tokio
#[tokio::main]
async fn main() {
// your async code here
}

#[tokio::main] คือ procedural macro ที่ห่อ async fn main ของคุณในการเรียก tokio::runtime::Runtime::block_on เพื่อเริ่ม tokio event loop

async fn compile เป็น struct นิรนามที่ implement Future — หนึ่ง field ต่อค่าที่ต้องอยู่รอดข้าม .await แต่ละจุด และ method poll ที่เป็น state machine กระโดดไปยังจุดที่มันคืนค่า Pending ครั้งล่าสุด .await ไม่ใช่ keyword ที่ “รอ” ในความหมายบล็อก thread — มันใกล้เคียงกับ return Poll::Pending พร้อม bookmark ที่ส่งกลับขึ้นไปให้ผู้เรียก poll ไม่มีอะไร “รันเบื้องหลัง” ด้วยตัวมันเอง — executor (tokio หรือ block_on ของเล่นในบทนี้) ต้องเรียก poll ซ้ำ ๆ เพื่อให้เกิดความคืบหน้าใด ๆ เลย

Future แทบไม่มีต้นทุนเลยจนกว่าจะถูก poll — คุณสร้างและเก็บได้เป็นพันตัวโดยไม่มี overhead ของ thread หรือ memory เกินกว่า field ของ state machine เอง แต่ laziness นั้นหมายความว่าความถูกต้องตอนนี้ขึ้นอยู่กับว่า มีอะไรบางอย่าง poll ทุก future ที่คุณสนใจจริง ๆ — การเรียก async fn ที่คุณไม่เคย .await และไม่เคยส่งให้ tokio::spawn จะไม่รันเลย เงียบ ๆ โดยไม่มี error thread รับประกันความคืบหน้าทันทีที่ spawn ด้วยต้นทุน OS จริง future ไม่รับประกันอะไรเลยจนกว่า executor จะขับเคลื่อนมัน ด้วยต้นทุนที่ใกล้ศูนย์

  • “.await บล็อก thread ปัจจุบันขณะรอ” มันระงับ task ปัจจุบัน ไม่ใช่ thread — executor เป็นอิสระที่จะรัน task อื่นบน OS thread เดียวกันขณะที่ task นี้ pending อยู่ std::thread::sleep บล็อก thread จริง ๆ ส่วน tokio::time::sleep(...).await ไม่บล็อก
  • “การเรียก async fn เริ่มทำงานทันทีเหมือนฟังก์ชันธรรมดา” การเรียก greet("Rust") แค่สร้างค่า Future เท่านั้น — ตามที่ demo block_on ข้างบนแสดงตรง ๆ println! ใน body ไม่รันจนกว่าจะมีอะไร poll มัน
  • “tokio กับ async/await คือสิ่งเดียวกัน” async/.await และ trait Future เป็นส่วนหนึ่งของภาษาและ std ส่วน tokio เป็น executor ตัวหนึ่ง (ที่ใช้กันแพร่หลาย) จากหลายตัว (async-std, smol) ที่รู้วิธีรัน future เหล่านั้นจริง ๆ

โค้ด async Rust ที่ AI เขียนมักเลือก std::sync::Mutex (ตัว synchronous จากบท threads) ภายใน body ของ async fn แล้วถือ guard ข้าม .await เพราะโค้ด “ดูเหมือนกัน” กับตัวอย่างที่ใช้ thread ที่มันถูกเทรนมา — โดยไม่สังเกตความเสี่ยงด้าน liveness ระดับ executor ที่เกิดขึ้น

🐛 หา bug ในโค้ดนี้

use std::sync::Mutex;
use std::time::Duration;
async fn record_event(log: &Mutex<Vec<String>>, event: String) {
let mut guard = log.lock().unwrap();
guard.push(event);
tokio::time::sleep(Duration::from_millis(10)).await; // simulate flushing to disk
println!("logged, {} entries so far", guard.len());
}
เฉลย

MutexGuard จาก log.lock() ยังมีชีวิตอยู่ข้าม tokio::time::sleep(...).await — executor สามารถระงับ task นี้ตรงนั้นแล้วรัน task อื่นบน thread เดียวกันที่ต้องการ lock ของ log ด้วย ตอนนี้ task นั้นก็ติดรอ guard ที่จะไม่ถูกปล่อยจนกว่า task นี้จะถูก poll อีกครั้ง ทางแก้: ทำ push และอ่าน guard.len() ก่อน .await โดย drop guard ก่อน — หรือเปลี่ยนไปใช้ lock แบบ async-aware (tokio::sync::Mutex) ถ้าจำเป็นต้องถือมันข้ามจุดระงับจริง ๆ

async fn ใน Rust ส่งคืนอะไร?
ทำไม Rust จึงต้องการ external executor อย่าง tokio เพื่อรัน async code?
ความแตกต่างระหว่าง tokio::join! และการ await แบบ sequential คืออะไร?
demo block_on พิสูจน์อะไรด้วยการพิมพ์ "after calling greet(), before polling" ก่อน "greet body is running"?
ทำไมการถือ guard ของ std::sync::Mutex ข้ามจุด .await ถึงเป็น smell?
เมื่อใดควรใช้ spawn_blocking แทน tokio::spawn ธรรมดา?