# Async/Await ใน Rust: อธิบาย Tokio, Futures และ Asynchronous Concurrency อย่างครบถ้วน > บทความอธิบายการเขียนโปรแกรมแบบ Asynchronous ใน Rust ด้วย async/await, Tokio runtime และ Futures ครอบคลุมตั้งแต่พื้นฐานจนถึงแนวทางปฏิบัติขั้นสูงสำหรับระบบที่ต้องการประสิทธิภาพสูง - Published: 2026-04-30 - Updated: 2026-04-30 - Author: SharpSkill - Tags: rust, async, tokio, futures, concurrency - Reading time: 12 min --- การเขียนโปรแกรมแบบ Asynchronous ถือเป็นหนึ่งในจุดแข็งที่สำคัญที่สุดของภาษา Rust โดยเฉพาะอย่างยิ่งสำหรับงานที่ต้องจัดการกับ I/O จำนวนมาก เช่น เว็บเซิร์ฟเวอร์ ระบบฐานข้อมูล หรือแอปพลิเคชันเครือข่ายที่ต้องรองรับการเชื่อมต่อพร้อมกันหลายพันรายการ Rust นำเสนอระบบ async/await ที่มีความปลอดภัยในระดับ memory safety และ concurrency safety โดยไม่ต้องพึ่งพา garbage collector บทความนี้จะอธิบายแนวคิดหลักของ Asynchronous Programming ใน Rust ตั้งแต่ Future trait พื้นฐาน ไปจนถึงการใช้งาน Tokio runtime สำหรับงานจริง พร้อมตัวอย่างโค้ดที่สามารถนำไปประยุกต์ใช้ได้ทันที > **เหมาะสำหรับนักพัฒนาที่มีพื้นฐาน Rust** > > บทความนี้เหมาะสำหรับผู้ที่มีความรู้พื้นฐานเกี่ยวกับ Rust เช่น ownership, borrowing และ trait แล้ว หากยังไม่คุ้นเคยกับแนวคิดเหล่านี้ ควรศึกษาพื้นฐานของ Rust ก่อนจะช่วยให้เข้าใจเนื้อหาได้ดียิ่งขึ้น ## Future Trait: หัวใจของระบบ Async ใน Rust ทุกอย่างที่เกี่ยวข้องกับ async ใน Rust เริ่มต้นจาก `Future` trait ซึ่งเป็นนามธรรม (abstraction) ที่แทนค่าซึ่งอาจจะพร้อมใช้งานในอนาคต แตกต่างจากภาษาอื่น ๆ อย่าง JavaScript ที่ Promise จะเริ่มทำงานทันทีที่ถูกสร้าง Future ใน Rust จะไม่ทำงานใด ๆ จนกว่าจะถูก poll อย่างชัดเจน แนวคิดนี้เรียกว่า "lazy evaluation" ```rust // core::future::Future trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; } ``` เมื่อ runtime เรียก `poll()` จะได้รับค่าเป็น `Poll::Ready(value)` หาก Future ทำงานเสร็จแล้ว หรือ `Poll::Pending` หากยังต้องรอการดำเนินการบางอย่าง เช่น การอ่านข้อมูลจากเครือข่าย ในกรณี `Pending` ระบบจะลงทะเบียน waker ไว้เพื่อแจ้งเตือน runtime เมื่อ Future พร้อมที่จะถูก poll อีกครั้ง สิ่งที่ทำให้ Rust แตกต่างจากภาษาอื่นคือ Future เป็น zero-cost abstraction กล่าวคือ คอมไพเลอร์จะแปลงโค้ด async ให้เป็น state machine ที่มีประสิทธิภาพสูงในขั้นตอนการคอมไพล์ โดยไม่มีค่าใช้จ่ายเพิ่มเติมในขณะรันไทม์ ## เริ่มต้นใช้งาน Tokio Runtime Rust ไม่ได้มี async runtime มาพร้อมกับตัวภาษา จึงต้องเลือกใช้ runtime จากภายนอก โดย Tokio เป็น runtime ที่ได้รับความนิยมมากที่สุดและเป็นมาตรฐานของอุตสาหกรรม รองรับทั้ง multi-threaded scheduler, I/O driver, timer และเครื่องมือ synchronization ต่าง ๆ ขั้นแรก ให้เพิ่ม Tokio เป็น dependency ในไฟล์ `Cargo.toml` ดังนี้ ```toml # Cargo.toml [dependencies] tokio = { version = "2", features = ["rt-multi-thread", "macros", "net", "time"] } ``` จากนั้นสามารถเขียนโปรแกรม async แรกได้ดังนี้ ```rust // main.rs #[tokio::main] async fn main() { let result = fetch_data().await; println!("Got: {result}"); } async fn fetch_data() -> String { // Simulate async I/O tokio::time::sleep(std::time::Duration::from_millis(100)).await; String::from("data loaded") } ``` `#[tokio::main]` macro จะสร้าง Tokio runtime และรันฟังก์ชัน `main` ภายใน runtime นั้น คีย์เวิร์ด `async` ทำให้ฟังก์ชันคืนค่าเป็น Future และ `.await` ใช้สำหรับรอผลลัพธ์ของ Future โดยจะ yield การควบคุมกลับไปยัง runtime เมื่อ Future ยังไม่พร้อม ทำให้ thread สามารถไปทำงานอื่นได้ในระหว่างที่รอ ## การทำ Concurrency ด้วย tokio::spawn จุดแข็งของ async programming คือความสามารถในการรันหลาย task พร้อมกัน `tokio::spawn` สร้าง task ใหม่ที่ทำงานแบบ concurrent บน Tokio runtime โดย task แต่ละตัวจะถูก schedule บน thread pool อย่างมีประสิทธิภาพ ```rust // concurrent_tasks.rs use tokio::task::JoinHandle; #[tokio::main] async fn main() { // Spawn two independent tasks let handle_a: JoinHandle = tokio::spawn(async { expensive_computation("dataset_a").await }); let handle_b: JoinHandle = tokio::spawn(async { expensive_computation("dataset_b").await }); // Await both results let (result_a, result_b) = ( handle_a.await.expect("task A panicked"), handle_b.await.expect("task B panicked"), ); println!("Results: {result_a}, {result_b}"); } async fn expensive_computation(name: &str) -> u32 { tokio::time::sleep(std::time::Duration::from_secs(1)).await; println!("{name} done"); 42 } ``` `tokio::spawn` คืนค่า `JoinHandle` ซึ่งสามารถ `.await` เพื่อรับผลลัพธ์จาก task ได้ สิ่งสำคัญคือ task ที่ถูก spawn จะเริ่มทำงานทันทีแบบ concurrent กับ task อื่น ๆ โดยไม่ต้องรอจนกว่าจะเรียก `.await` บน handle > **ข้อควรระวังเรื่อง 'static lifetime** > > task ที่ถูก spawn ด้วย `tokio::spawn` จะต้องเป็น `'static` หมายความว่าไม่สามารถ borrow ข้อมูลจากขอบเขตภายนอกได้โดยตรง ต้องใช้ `move` closure หรือ `Arc` สำหรับการแชร์ข้อมูลระหว่าง task ## tokio::join! สำหรับ Structured Concurrency เมื่อต้องการรัน Future หลายตัวพร้อมกันและรอผลลัพธ์ทั้งหมด `tokio::join!` เป็นเครื่องมือที่เหมาะสมที่สุด แตกต่างจาก `spawn` ตรงที่ `join!` ไม่ต้องการ `'static` lifetime และจะรอจนกว่า Future ทุกตัวจะเสร็จสมบูรณ์ ```rust // join_example.rs use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { // All three run concurrently, total time ~200ms (not 600ms) let (users, orders, inventory) = tokio::join!( fetch_users(), fetch_orders(), fetch_inventory() ); println!("Users: {}, Orders: {}, Stock: {}", users.len(), orders.len(), inventory); } async fn fetch_users() -> Vec { sleep(Duration::from_millis(200)).await; vec!["Alice".into(), "Bob".into()] } async fn fetch_orders() -> Vec { sleep(Duration::from_millis(150)).await; vec!["ORD-001".into()] } async fn fetch_inventory() -> u32 { sleep(Duration::from_millis(100)).await; 84 } ``` ในตัวอย่างนี้ การดึงข้อมูลทั้ง 3 รายการจะทำงานพร้อมกัน ทำให้ใช้เวลารวมเพียงประมาณ 200 มิลลิวินาที (เท่ากับ Future ที่ช้าที่สุด) แทนที่จะเป็น 450 มิลลิวินาทีหากทำงานแบบลำดับ นี่คือประโยชน์หลักของ structured concurrency ที่ช่วยลดเวลาการประมวลผลอย่างมีนัยสำคัญ ## tokio::select! สำหรับ Racing Futures ในบางสถานการณ์ ต้องการรอเฉพาะ Future ตัวแรกที่เสร็จก่อน เช่น การดึงข้อมูลจาก cache และ database พร้อมกัน แล้วใช้ผลลัพธ์จากแหล่งที่ตอบกลับเร็วที่สุด `tokio::select!` ตอบโจทย์การใช้งานลักษณะนี้ ```rust // select_example.rs use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { tokio::select! { val = fetch_from_cache() => { println!("Cache hit: {val}"); } val = fetch_from_database() => { println!("DB result: {val}"); } } } async fn fetch_from_cache() -> String { sleep(Duration::from_millis(5)).await; "cached_value".into() } async fn fetch_from_database() -> String { sleep(Duration::from_millis(50)).await; "db_value".into() } ``` `select!` จะยกเลิก (cancel) Future อื่น ๆ ที่ยังไม่เสร็จโดยอัตโนมัติเมื่อมี Future ตัวใดตัวหนึ่งเสร็จก่อน ทำให้ไม่สิ้นเปลืองทรัพยากร เหมาะอย่างยิ่งสำหรับการทำ timeout, circuit breaker หรือ fallback pattern ## การจัดการ Error ในโค้ด Async การจัดการ error ในโค้ด async ของ Rust ใช้หลักการเดียวกับโค้ด synchronous คือใช้ `Result` type ร่วมกับ `?` operator สิ่งสำคัญคือการออกแบบ error type ที่ครอบคลุมข้อผิดพลาดที่อาจเกิดขึ้นจากหลายแหล่ง ```rust // error_handling.rs use std::io; #[derive(Debug)] enum AppError { Network(reqwest::Error), Parse(serde_json::Error), Io(io::Error), } async fn load_config(url: &str) -> Result { let response = reqwest::get(url) .await .map_err(AppError::Network)?; let text = response.text() .await .map_err(AppError::Network)?; let config: Config = serde_json::from_str(&text) .map_err(AppError::Parse)?; Ok(config) } #[derive(serde::Deserialize)] struct Config { db_url: String, port: u16, } ``` การสร้าง enum สำหรับ error ทำให้สามารถจัดการข้อผิดพลาดจากหลายแหล่งได้อย่างเป็นระบบ ไม่ว่าจะเป็น network error, parsing error หรือ I/O error โดยคอมไพเลอร์จะบังคับให้จัดการ error ทุกกรณี ซึ่งช่วยป้องกัน runtime error ที่ไม่คาดคิด ## Async Channels สำหรับการสื่อสารระหว่าง Task เมื่อ task หลายตัวต้องสื่อสารกัน Tokio มี channel หลายประเภทให้เลือกใช้ `mpsc` (multi-producer, single-consumer) เป็นรูปแบบที่พบบ่อยที่สุดสำหรับ pattern แบบ producer-consumer ```rust // channel_example.rs use tokio::sync::mpsc; #[tokio::main] async fn main() { // Bounded channel with capacity 32 let (tx, mut rx) = mpsc::channel::(32); // Producer task let producer = tokio::spawn(async move { for i in 0..5 { tx.send(format!("message-{i}")).await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(10)).await; } // tx dropped here, closing the channel }); // Consumer reads until channel closes while let Some(msg) = rx.recv().await { println!("Received: {msg}"); } producer.await.unwrap(); } ``` Bounded channel มีข้อดีคือสามารถสร้าง backpressure ได้ เมื่อ channel เต็ม producer จะถูก suspend จนกว่า consumer จะอ่านข้อมูลออกไป ช่วยป้องกันปัญหา memory ล้นในกรณีที่ producer ทำงานเร็วกว่า consumer ## การจำกัดจำนวน Concurrent Tasks ด้วย Semaphore ในการใช้งานจริง มักจะต้องจำกัดจำนวนงานที่ทำพร้อมกัน เช่น การส่ง HTTP request ไปยัง API ภายนอก เพื่อไม่ให้เกิดปัญหา rate limiting หรือทรัพยากรหมด Semaphore เป็นเครื่องมือที่เหมาะสำหรับการควบคุมจำนวน concurrent operations ```rust // rate_limited_fetcher.rs use std::sync::Arc; use tokio::sync::Semaphore; async fn fetch_all(urls: Vec, max_concurrent: usize) -> Vec> { let semaphore = Arc::new(Semaphore::new(max_concurrent)); let mut handles = Vec::new(); for url in urls { let sem = Arc::clone(&semaphore); let handle = tokio::spawn(async move { // Acquire permit before making request let _permit = sem.acquire().await.unwrap(); reqwest::get(&url) .await .map(|r| r.status().to_string()) .map_err(|e| e.to_string()) // permit dropped here, allowing next task to proceed }); handles.push(handle); } let mut results = Vec::new(); for handle in handles { results.push(handle.await.unwrap()); } results } ``` Semaphore ทำงานโดยกำหนดจำนวน permit ที่มีอยู่ ในตัวอย่างนี้ `max_concurrent` กำหนดจำนวน request สูงสุดที่สามารถทำพร้อมกันได้ เมื่อ task ได้รับ permit จะเริ่มทำงาน และเมื่อเสร็จแล้ว permit จะถูกคืนให้ task ถัดไปโดยอัตโนมัติ ## การจัดการงาน CPU-Intensive ด้วย spawn_blocking สิ่งสำคัญที่ต้องเข้าใจคือ async runtime ของ Tokio ถูกออกแบบมาสำหรับงาน I/O-bound เป็นหลัก หากมีงานที่ใช้ CPU หนัก เช่น การเข้ารหัส การบีบอัดข้อมูล หรือการคำนวณ hash ไม่ควรรันบน async thread pool โดยตรง เพราะจะบล็อก thread และทำให้ task อื่น ๆ ไม่สามารถทำงานได้ ```rust // spawn_blocking_example.rs #[tokio::main] async fn main() { let hash = tokio::task::spawn_blocking(|| { // CPU-intensive work runs on a blocking thread compute_hash(b"large dataset") }) .await .unwrap(); println!("Hash: {hash}"); } fn compute_hash(data: &[u8]) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); data.hash(&mut hasher); format!("{:x}", hasher.finish()) } ``` `spawn_blocking` จะย้ายงานไปรันบน thread pool แยกต่างหากที่ออกแบบมาสำหรับงานที่บล็อก thread ทำให้ async thread pool ยังคงทำงานได้อย่างราบรื่นสำหรับงาน I/O อื่น ๆ > **หลักการเลือกใช้ spawn vs spawn_blocking** > > ใช้ `tokio::spawn` สำหรับงาน async/I/O-bound เช่น network request, database query หรือ file I/O ส่วน `tokio::task::spawn_blocking` ใช้สำหรับงาน CPU-bound เช่น การเข้ารหัส การบีบอัด หรือการประมวลผลข้อมูลจำนวนมาก การเลือกใช้ให้ถูกต้องเป็นกุญแจสำคัญสู่ประสิทธิภาพที่ดี ## แนวทางปฏิบัติที่ดีสำหรับ Async Rust จากประสบการณ์การใช้งาน async Rust ในระบบ production สามารถสรุปแนวทางปฏิบัติที่สำคัญได้ดังนี้ **หลีกเลี่ยงการ block async thread** อย่าใช้ `std::thread::sleep` หรือการดำเนินการที่บล็อกบน async context ให้ใช้ `tokio::time::sleep` แทน หรือใช้ `spawn_blocking` สำหรับงานที่จำเป็นต้องบล็อก **ใช้ bounded channel เสมอ** เพื่อสร้าง backpressure และป้องกันปัญหา memory leak ในกรณีที่ producer ทำงานเร็วกว่า consumer **ระมัดระวังเรื่อง cancellation safety** เมื่อใช้ `select!` ควรตรวจสอบว่า Future ที่ถูกยกเลิกจะไม่ทิ้งข้อมูลสำคัญ เพราะ Future จะถูก drop ทันทีที่ถูกยกเลิก **ออกแบบ error handling อย่างรอบคอบ** สร้าง error type ที่ครอบคลุมและใช้ `?` operator เพื่อส่งต่อ error ขึ้นไปยัง caller อย่างเป็นระบบ **ใช้ structured concurrency เมื่อเป็นไปได้** `tokio::join!` เหมาะกว่า `tokio::spawn` เมื่อ task ทั้งหมดมี lifetime เดียวกัน เพราะจัดการ lifetime ง่ายกว่าและไม่ต้องใช้ `'static` bound ## สรุป ระบบ async/await ของ Rust ร่วมกับ Tokio runtime มอบเครื่องมือที่ครบครันสำหรับการเขียนโปรแกรมแบบ asynchronous ที่มีทั้งประสิทธิภาพสูงและความปลอดภัย ตั้งแต่ Future trait พื้นฐาน, การทำ concurrency ด้วย `spawn` และ `join!`, การแข่ง Future ด้วย `select!`, การสื่อสารระหว่าง task ด้วย channel, การจำกัด concurrency ด้วย Semaphore ไปจนถึงการจัดการงาน CPU-intensive ด้วย `spawn_blocking` ความเข้าใจในเครื่องมือเหล่านี้และหลักการเลือกใช้ให้เหมาะสมกับแต่ละสถานการณ์ เป็นทักษะที่จำเป็นสำหรับนักพัฒนา Rust ที่ต้องสร้างระบบที่รองรับ traffic สูงและต้องการประสิทธิภาพระดับ production การฝึกฝนผ่านการเขียนโค้ดจริงและทำความเข้าใจกลไกภายในของ Tokio จะช่วยให้สามารถออกแบบระบบ async ที่แข็งแกร่งและบำรุงรักษาง่ายได้อย่างมั่นใจ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/rust/rust-async-await-tokio-futures-concurrency