執行緒池(thread pool)
想像一間忙碌的餐廳。最天真的做法,是每接到一張單就重新雇一名服務生,帶他走完到職流程,讓他端一盤菜,然後解雇他。這很荒謬:招募與解雇的成本遠遠蓋過真正的工作。理智的做法,是在現場固定留一批服務生;每個人從出菜架上抓下一張單,送出去,再回來拿下一張。執行緒池就是程式裡的這批固定班底——一組一開始就建立、長期存活的工作者執行緒,它們從共享佇列拉取工作來執行。
具體而言,執行緒池握有 N 個工作者執行緒(N 常設在 CPU 核心數附近)和一個任務佇列。當你的程式想並行完成某件事時,它不呼叫 pthread_create(),而是把一個任務(一個函式加上它的資料)提交到佇列上。空閒的工作者把任務取出、執行到完成,再回頭等下一個。建立一條執行緒代價昂貴——作業系統必須配置一塊堆疊(常見是數百 KiB 到幾 MiB)、設好簿記資料,排程器也得認識它。執行緒池在啟動時把這筆代價付 N 次,而不是每個任務付一次,於是提交任務變得很便宜:就只是一次入佇列。
有兩點讓執行緒池勝過「每任務一執行緒」。其一是攤提:成千上萬個任務共用同一小撮執行緒,每個任務的額外開銷便微乎其微。其二是有界平行度:用每任務一執行緒,一萬個同時湧入的請求會生出一萬條執行緒,耗盡記憶體並把排程器拖垮;而一個有 8 個工作者的池,一次最多只跑 8 個任務,其餘的乖乖在佇列裡等。誠實的提醒:池大小是真正要調的旋鈕。太小,CPU 密集的任務就讓核心閒置;若你照 CPU 工作量設定大小,卻讓某個工作者卡在 I/O 上阻塞,就可能拖死整個池——詳見「別阻塞執行器」那條規則。
粗略的成本圖像:pthread_create() 約數十微秒、每條執行緒約 1 MiB 堆疊;而提交到一個熱身好的池,約是一次上鎖加入佇列(數十奈秒)。Java:ExecutorService pool = Executors.newFixedThreadPool(8); pool.submit(task);
提交一個任務很便宜,因為執行緒早已存在;執行緒池把建立成本攤提到所有任務上。
若一個依 CPU 工作量設定大小的池讓任務發生阻塞,就可能死結或停滯——例如某任務等待一個只有被它排在後面的另一任務才能產生的結果。有界平行度是雙面刃。