Async Introduction
Async/Await และ Futures
หัวข้อที่มีชื่อว่า “Async/Await และ Futures”syntax async/.await ของ Rust ให้คุณเขียนโค้ด non-blocking ที่ดูเหมือนโค้ด sequential ธรรมดา ภายใต้ประทุน async fn ส่งคืน Future — ค่าที่แสดงถึงการคำนวณที่อาจยังไม่เสร็จสมบูรณ์
// @skip-verify crate: tokiouse 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 คืออะไร?
หัวข้อที่มีชื่อว่า “Future คืออะไร?”Future<Output = T> เป็น trait ที่แสดงถึงค่าประเภท T ที่จะพร้อมในบางจุดในอนาคต การเรียก async fn ไม่ได้รัน body ทันที — แต่ส่งคืน Future กลับมา body จะรันเฉพาะตอนคุณ .await (หรือ poll ผ่าน executor)
// @skip-verify crate: tokio// This does nothing yet — just creates a Futurelet fut = fetch_data(42);
// This runs the body and blocks until completelet result = fut.await;การประเมินแบบ lazy นี้คือเหตุผลที่ async ของ Rust มีต้นทุนเป็นศูนย์: future ที่ไม่เคย await จะไม่รันเลย
พิสูจน์ให้ดู: Executor จิ๋วที่ไม่ต้องใช้ Crate เลย
หัวข้อที่มีชื่อว่า “พิสูจน์ให้ดู: Executor จิ๋วที่ไม่ต้องใช้ Crate เลย”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}");}Compiling…
before calling greet()after calling greet(), before pollinggreet body is running for RustHello, 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 คือจุดที่คอมไพเลอร์สามารถระงับ state machine ที่สร้างขึ้นและคืนค่า Poll::Pending ได้ หน้าที่ของ executor (ตัวจริงของ tokio หรือ block_on ของเล่นในบทนี้) คือเรียก poll ซ้ำทุกครั้งที่มีโอกาสที่ future จะคืบหน้าต่อได้
การรัน Task พร้อมกันด้วย tokio::join!
หัวข้อที่มีชื่อว่า “การรัน Task พร้อมกันด้วย tokio::join!”เพื่อรัน async task หลายตัวในเวลาเดียวกัน ใช้ tokio::join! ซึ่ง drive future ทั้งหมดพร้อมกันบน thread เดียว (หรือข้าม worker thread ขึ้นอยู่กับ runtime configuration)
// @skip-verify crate: tokiouse 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! รอจนทุกตัวเสร็จและส่งคืนผลลัพธ์ตามลำดับ
การ Spawn Async Task
หัวข้อที่มีชื่อว่า “การ Spawn Async Task”tokio::spawn เปิดตัว task บน tokio thread pool แล้วส่งคืน JoinHandle คล้ายกับ std::thread::spawn
// @skip-verify crate: tokiouse 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);}Compiler พูดว่าอย่างไร: Future ที่ Spawn ต้องเป็น Send
หัวข้อที่มีชื่อว่า “Compiler พูดว่าอย่างไร: Future ที่ Spawn ต้องเป็น Send”tokio::spawn (เหมือน std::thread::spawn) อาจรัน future ของมันบน worker thread ที่ต่างจาก thread ที่สร้างมัน ดังนั้น future — และทุกอย่างที่มันถืออยู่ข้าม .await — ต้องเป็น Send การถือ Rc<T> ข้าม .await ทำให้กฎนี้พัง:
// @skip-verify crate: tokiouse 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 laternote: 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 function175 | where176 | 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
Async Smell สองแบบที่เจอเฉพาะใน Async
หัวข้อที่มีชื่อว่า “Async Smell สองแบบที่เจอเฉพาะใน Async”การถือ Mutex แบบ synchronous ข้าม .await ดูเหมือนไม่มีปัญหาสำหรับ borrow checker แต่เป็นความเสี่ยงด้าน liveness จริง: executor สามารถระงับ task ของคุณกลาง .await ขณะที่ยังถือ guard อยู่ และถ้า executor นั้นบังเอิญรัน task อื่นบน thread เดียวกัน ที่ต้องการ lock นั้นด้วย คุณจะได้ deadlock ที่ type system มองไม่เห็น (บั๊กคลาสเดียวกับ deadlock สองล็อคในบท threads แต่คราวนี้มีแค่ lock เดียว)
// @skip-verify crate: tokiouse 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: tokioasync 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 ว่างอยู่
async fn ใน Trait
หัวข้อที่มีชื่อว่า “async fn ใน Trait”ตั้งแต่ 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 = ...> เอง
async vs Threads: ควรใช้อะไร
หัวข้อที่มีชื่อว่า “async vs Threads: ควรใช้อะไร”| เกณฑ์ | 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) |
การเพิ่ม tokio ในโปรเจกต์
หัวข้อที่มีชื่อว่า “การเพิ่ม tokio ในโปรเจกต์”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
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 ซ้ำ ๆ เพื่อให้เกิดความคืบหน้าใด ๆ เลย
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Future แทบไม่มีต้นทุนเลยจนกว่าจะถูก poll — คุณสร้างและเก็บได้เป็นพันตัวโดยไม่มี overhead ของ thread หรือ memory เกินกว่า field ของ state machine เอง แต่ laziness นั้นหมายความว่าความถูกต้องตอนนี้ขึ้นอยู่กับว่า มีอะไรบางอย่าง poll ทุก future ที่คุณสนใจจริง ๆ — การเรียก async fn ที่คุณไม่เคย .await และไม่เคยส่งให้ tokio::spawn จะไม่รันเลย เงียบ ๆ โดยไม่มี error thread รับประกันความคืบหน้าทันทีที่ spawn ด้วยต้นทุน OS จริง future ไม่รับประกันอะไรเลยจนกว่า executor จะขับเคลื่อนมัน ด้วยต้นทุนที่ใกล้ศูนย์
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
.awaitบล็อก thread ปัจจุบันขณะรอ” มันระงับ task ปัจจุบัน ไม่ใช่ thread — executor เป็นอิสระที่จะรัน task อื่นบน OS thread เดียวกันขณะที่ task นี้ pending อยู่std::thread::sleepบล็อก thread จริง ๆ ส่วนtokio::time::sleep(...).awaitไม่บล็อก - “การเรียก async fn เริ่มทำงานทันทีเหมือนฟังก์ชันธรรมดา” การเรียก
greet("Rust")แค่สร้างค่าFutureเท่านั้น — ตามที่ demoblock_onข้างบนแสดงตรง ๆprintln!ใน body ไม่รันจนกว่าจะมีอะไร poll มัน - “tokio กับ async/await คือสิ่งเดียวกัน”
async/.awaitและ traitFutureเป็นส่วนหนึ่งของภาษาและstdส่วนtokioเป็น executor ตัวหนึ่ง (ที่ใช้กันแพร่หลาย) จากหลายตัว (async-std,smol) ที่รู้วิธีรัน future เหล่านั้นจริง ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ด 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) ถ้าจำเป็นต้องถือมันข้ามจุดระงับจริง ๆ