任務與任務佇列(task queue)
想像熟食櫃檯放著一台「抽號碼」的機器。每位顧客是一份工作;托盤裡依序排著的號碼牌就是等候名單,店員從前往後叫號。任務就是這樣一份工作單位——一塊自成一體、被打包起來以便稍後執行的計算。任務佇列則是那個托盤,盛裝著等待工作者來取走的任務。
在程式碼裡,任務(也叫工作項目或 job)通常是一個閉包或一個小物件:一個要呼叫的函式,加上它需要的資料(它擷取到的引數)。它刻意不自帶任何執行緒——它只是一份工作的描述,在某個工作者執行它之前是惰性的。任務佇列是一個並行的先進先出結構,生產者把任務推進去、工作者執行緒把任務彈出來。因為多條執行緒會同時碰它,它必須能安全地並行存取——用鎖保護,或做成無鎖佇列。簡單的池只有一個共享佇列;更講究的排程器則給每個工作者各自一個佇列(雙端佇列),並讓空閒的工作者去偷別人的。
把「任務」和「佇列」拆開,正是讓你把「有哪些工作要做」與「由哪條執行緒、在何時執行」分離開來的核心概念。它無所不在:執行緒池的待辦堆、事件迴圈的待處理回呼、GPU 的命令佇列、訊息中介。給初學者最關鍵的觀念轉變是:任務是資料,不是一個正在進行的活動。提交一個任務本身不會啟動任何東西;它只是把工作放到工作者終究會找到的地方。如果始終沒有工作者來清空佇列,任務就會永遠擱在那裡。
把任務寫成閉包(Rust):let task = move || { process(data); }; pool.spawn(task); // 在某個工作者呼叫它之前,'task' 只是一個值。
這個閉包擷取了它的資料並被排入佇列;在工作者把它彈出並呼叫之前,什麼也不會執行。
無界的任務佇列可能藏著記憶體洩漏:若生產者提交的速度快過工作者清空的速度,待辦堆會無止境地增長,直到耗盡記憶體。為佇列設上界(並施加背壓)才是解方。