資料平行相對於任務平行(data vs task parallelism)
讓許多廚師開工有兩種方式。方式一:給每位廚師同一份工作——「你們每個人削這堆馬鈴薯」——並把一大堆馬鈴薯分給他們。方式二:給每位廚師不同的工作——一個切洋蔥、一個攪醬汁、一個烤麵包——全部同時進行。前者是資料平行:把同一個操作平行地套到許多份資料上。後者是任務平行:不同的操作平行地跑,各做各自不同的事。
資料平行把資料切開、在每一塊上同時跑相同的工作:把一個一百萬元素陣列的每個元素都加 1.0,做法是給每個核心一個切片;或把一個龐大資料集求和,做法是讓每個工作者加總一部分、再合併起來(一次 map-reduce)。工作是一致的;不同的只是每個工作者握著哪一片資料。這天生契合 parallel-for、GPU(數千個核心對不同資料做同一條指令)、以及 CPU 上的 SIMD 指令。任務平行則把工作切成不同的活動、讓它們並行跑:在一個網頁請求裡,抓使用者個人資料、查推薦服務、載入廣告——三個不相關的任務同時進行,各是不同的程式碼。工作是異質的;它們共享的(若有的話)只是最後把各自的結果合併起來。分叉-合併兩者都能表達,但任務平行是子任務真正做不同事情的地方,而資料平行是同一件事的許多份複本。
這區分之所以重要,是因為它把你指向正確的工具與正確的上限。當每個元素的工作彼此獨立時,資料平行隨著更多核心漂亮地擴展——這正是 GPU 與 SIMD 為之而生的,而且它通常較容易推理,因為每個工作者跑的是同一份程式碼。任務平行則受限於你有多少個真正獨立的活動、以及它們如何彼此相依(它們的相依圖),還有 Amdahl's 定律對它們之間串列黏合處的限制。誠實的提醒:兩者並不互斥——真實系統會混用它們(一條任務平行的管線,其每個階段各自是資料平行的)。而當任務碰觸共享可變狀態時,兩者都不免除同步的需要;「平行」描述的是工作的形狀,不是安全的保證。
資料平行(一個操作、許多資料):pixels.par_iter_mut().for_each(|p| *p = brighten(*p)); ——每個工作者在自己的切片上跑 brighten。任務平行(許多操作):join(fetch_profile(), fetch_ads(), fetch_recs()); ——三件不同的工作同時進行。
資料平行=同一份程式碼跑在不同資料上;任務平行=不同的程式碼並行地跑。
兩者並不互斥——真實的工作負載會把它們組合起來。而「平行」講的是工作的形狀,不是安全:當平行的工作者共享可變狀態時,無論是哪一種平行,你仍然需要同步。