進階並行與非同步

執行器/執行時期(executor / runtime)

一齣舞台劇裡,演員都背熟了台詞,但在導演於正確時刻把每個人叫上台、舞台監督在幕後盯著提示之前,什麼也不會發生。非同步程式一模一樣:你的非同步函式是懂得暫停與恢復的協程,但它們本身是惰性的。執行器就是真正讓它們跑起來的導演——它挑出就緒的任務,把每一個往前推進,直到它下次暫停。執行時期則是整個製作:執行器,加上那個盯著任務所等待之事件的反應器。

具體看這個迴圈。執行器握有一個就緒任務的佇列。它彈出一個並輪詢它:把協程一路跑到它撞上某個結果尚未就緒的 await。在那個暫停點,任務向反應器登記它的興趣——「這個通訊端可讀時叫醒我」——然後交回一個「未就緒」訊號加上一個喚醒器(一個把它重新排入佇列的回呼)。執行器轉身去處理下一個就緒任務,讓執行緒保持忙碌。同時,反應器守在某個作業系統的就緒機制上(epoll、kqueue、IOCP、io_uring)。當通訊端變成可讀,反應器觸發那個任務的喚醒器,把任務推回執行器的就緒佇列,下一輪執行器再次輪詢它——從它暫停的原地恢復。許多執行器跑著數條工作者執行緒,每條各有自己的就緒佇列,並用工作竊取來平衡它們。

這正是初學者忽略的環節。尤其在 Rust 裡,一個非同步函式回傳的 Future 本身什麼都不做——沒有執行器(Tokio、async-std)去輪詢它,工作就永遠不會跑;人們對自己的非同步程式碼「默默地什麼都沒做」百思不解。職責的劃分值得記住:執行器決定接下來哪個任務跑、跑在哪條執行緒上(排程器);反應器決定一個沉睡的任務何時被允許醒來(I/O 守望者)。兩者合起來,讓寥寥幾條執行緒去多工成千上萬個並行任務,每個任務只在它有工作時執行,並在它必須等待的那一刻讓出。

Rust:一個沒被驅動的 future 形同死物。#[tokio::main] async fn main() { spawn(task).await; } // Tokio 執行時期提供了執行器(輪詢任務)和反應器(由 epoll 驅動的喚醒)。

執行器=排程器(接下來跑什麼、跑在哪);反應器=盯著 I/O 的守望者,在被等待的事件發生時觸發喚醒器。

在 Rust 裡,Future 是惰性的:在執行器輪詢它之前什麼都不發生。「我的非同步程式碼沒跑」幾乎總是表示它從未被等待、或未被 spawn 到某個執行時期上。有些執行時期是單執行緒、有些是多執行緒——選錯會有實際後果。

又稱
runtimeschedulerreactor + executor執行時期排程器