進階並行與非同步

協作式排程與讓出點(cooperative scheduling and the yield point)

想像一場沒有主席、沒有議事槌的會議。大家能輪流發言,純粹是因為每位發言者自己會在某刻停下來說「我講完了,換誰?」。這運作得很漂亮——只要沒人霸佔發言權。協作式排程正是如此:任務一直跑,直到它在某個讓出點自願放棄執行緒,排程器才把控制權交給另一個任務。沒有人會被強制打斷。

拿它跟作業系統的執行緒排程器對照,後者是搶佔式的:計時器中斷一觸發,核心就在指令執行到一半時把 CPU 從一條執行緒搶走、交給另一條,不管那條執行緒願不願意。協作式排程沒有這種計時器;排程器只有在當前任務讓出時,才有機會挑一個新任務。在非同步程式碼裡,讓出點就是 await 點:每個 await 都是協程可能暫停、把控制權交還執行器的地方。在兩個 await 之間,任務不被打斷地跑——它完全獨佔那條工作者執行緒。所以「讓出」並不神奇;它就只是任務跑到一個結果尚未就緒的 await(或一個明確的 yield_now()),保存自己的狀態,回到執行器,好讓另一個就緒任務能跑。

它的吸引力在於效率與簡潔:在協作式任務之間切換,只是一次函式回返加一次重新輪詢——不勞核心介入、不必保存一整條執行緒的暫存器、沒有快取冷掉的上下文切換。這正是為何一條執行緒能用遠比作業系統多工執行緒便宜得多的代價,去協作式地多工成千上萬個非同步任務。代價與危險,正是「沒有人會被強制打斷」的反面:一個永遠到不了讓出點的任務——一段沒有 await 的緊密 CPU 迴圈——會永遠握著工作者執行緒,餓死同緒上的每一個其他任務。協作式排程只有在每個任務都肯協作時才公平。這就是「別阻塞執行器」那條規則背後的深層原因。

在兩個 await 之間,任務獨佔執行緒:loop { let x = recv().await; /* 讓出點 */ process(x); /* 不被打斷地跑到下一個 .await */ } 一個冗長且不含 .await 的 process() 會阻塞同處的每一個任務。

控制權只在 await 點切換;排程器無法搶佔一個正在執行的協作式任務。

協作式並不代表每個任務都延遲更低——一個沒有讓出點的貪心任務能獨佔一個工作者。有些執行時期(Go、較新的 Tokio)加上有限的搶佔或自動讓出來緩和它,但核心模型仍仰賴協作。

又稱
cooperative multitaskingyield point協作式多工讓出點