async/await 與協程(coroutines)
讀食譜時,你來到一步寫著「讓麵團發酵一小時」。愚笨的廚師會在碗前僵立一小時。明智的廚師則替食譜夾個書籤,去切另一道菜的蔬菜,等計時器響了再回來。async/await 讓一個函式正好能這麼做:在一個 await 點,它可以暫停——替自己夾書籤、退到一旁——讓緩慢的操作去完成,把執行緒釋放出來跑別的工作,之後再從原地恢復。
標記為 async 的函式就是一個協程:一個可以中途暫停、稍後再恢復的函式,不像普通函式那樣從頭跑到尾、絕不退場。await 標記一個暫停點。當你寫 let data = await fetch(url),意思是:啟動這個 fetch;若還沒好,就暫停這個協程、把執行緒交還給執行器去跑別的;等 fetch 完成,就在這一行原地恢復這個函式,並把 data 綁好。編譯器/執行時期會跨越這次暫停,保存協程的區域變數和它「我做到哪了」的位置。無堆疊(stackless)實作(Rust async、C++20 協程)把每個非同步函式編譯成一台狀態機——一個物件,它的「狀態」欄位記著它停在哪個 await。有堆疊(stackful)的(goroutine、纖程)則保有一整條獨立堆疊,直接把它停泊起來。無論哪種,暫停/恢復都是協作式的:協程在 await 點自願讓出,執行時期從不被迫去打斷它。
這之所以重要,是因為它讓一條執行緒能同時兜著上千個等待中的操作——一台處理一萬個閒置連線的網頁伺服器,需要的是一萬個便宜的暫停協程,而不是一萬條昂貴的作業系統執行緒。最關鍵的誠實要點、也是初學者最常絆倒的地方是:await 不代表「平行執行」,async 也不會把程式碼變成多執行緒。async/await 講的是「等待時不浪費執行緒」;它表達的是並行(許多任務同時在進行),這些任務可能、也可能不在多條執行緒上跑。而如果你的非同步函式在兩個 await 之間做了真正的阻塞工作,它就霸佔了執行緒、拖死同緒上的每一個其他任務——詳見「別阻塞執行器」那條規則。
async fn handle(c: Conn) { let req = read(c).await; let resp = db_query(req).await; write(c, resp).await; } // 三個 await 點,任務在那裡可以暫停;其間它握著執行緒。
在每個 .await,協程可能讓出執行緒;等待的結果就緒時,它在下一行恢復。
async/await 是並行,不必然是平行。它本身不生出執行緒,而一個沒有真正暫停的非同步函式仍是同步執行的。它的好處是在許多等待之間重複利用一條執行緒,而不是多加核心。