進階並行與非同步

工作竊取排程器(work-stealing scheduler)

想像一排收銀員,每人有自己的隊伍。如果所有人都擠進同一條共享隊伍,那道被爭搶的門就成了瓶頸。反過來,若每位收銀員各自帶一條私有隊伍,但在空閒時走過去服務忙碌鄰居隊伍的尾端,負載就能自我平衡,不需要一個中央交通警察。工作竊取排程器正是這樣跑任務的:每條工作者執行緒有自己的任務雙端佇列,空閒的工作者去偷忙碌工作者佇列裡的工作。

其結構是每個工作者一個雙端佇列(deque)。工作者在自己這一端——私有端——推入並彈出它新生出來的任務,像堆疊一樣:後進先出。這是刻意的。剛建立的任務通常是工作者此刻正在做之事的子任務,其資料在快取裡還是熱的,而後進先出讓工作者深入一棵工作子樹。當一個工作者的佇列空了,它隨機挑一個受害者,從另一端——尾端——偷取一個較舊、較大、很可能彼此獨立的任務。從遠端偷竊也把與擁有者(正忙於近端)的爭用降到最低。這就是 Tokio、Go 的 goroutine 排程器、Intel TBB 與 .NET 執行緒池背後的模型。

為何它勝出:它幾乎沒有中央協調,因此能擴展到多核;對分而治之(分叉-合併)的工作負載,它的效率有理論保證;而且它會自我平衡——提早做完的工作者不會閒著,它去幫忙。誠實的提醒:偷竊會碰到另一條執行緒的資料,因此並非免費(它要付一次同步操作和一次快取未命中),所以好的排程器靠每次偷一大塊來讓偷竊變得稀少;而若任務大小極不均勻,或某工作者選擇阻塞而非讓出,平衡便會受損。它是衝著吞吐量去的啟發式法,不是對任何單一任務延遲的保證。

擁有者把雙端佇列當後進先出的堆疊用(在頭端推入/彈出);竊賊則從尾端做先進先出:擁有者 -> push_bottom / pop_bottom(熱、便宜);竊賊 -> steal_top(冷、需同步)。隨機選受害者把負載分散開來。

熱的近期工作留在本地(頭端後進先出);空閒執行緒從尾端偷走又舊又大又獨立的工作。

工作竊取最佳化的是總吞吐量,不是公平性:一個任務可能在佇列裡擱很久才有人來偷。它也假設任務會跑到某個讓出點;一個阻塞了作業系統執行緒的工作者,會餓死它整個佇列。

又稱
work-stealing工作竊取