CodeGym /課程 /JAVA 25 SELF /平行運算入門

平行運算入門

JAVA 25 SELF
等級 54 , 課堂 0
開放

1. 多執行緒 vs 平行運算

多執行緒:數量很多,但未必同時

多執行緒是指你的程式同時擁有多個執行緒的執行脈絡。每個執行緒就像一條獨立的工作線:一個在計算,另一個在等待使用者輸入,第三個把資料寫入檔案。在 Java 中,你可以透過類別 Thread 建立執行緒、實作介面 Runnable,或使用像 ExecutorService 這樣的高階工具(關於它們——在下一講)。

但是! 多執行緒不保證你的任務真的同時執行。這取決於處理器的核心數。如果只有一個核心,執行緒只是彼此之間快速「切換」——快到讓人覺得彷彿同時進行。實際上,處理器在任一時刻只會執行一個執行緒,其他的則在等待輪到自己。

平行運算:任務真的同時進行

平行運算是指你的程式碼確實在多個處理器核心上同時執行。如果你的電腦有 4816 個核心——只要把大型任務切成彼此獨立的部分並分配到各核心,就能真正加速處理。

打個比方,多執行緒就像只有一位廚師,在煮羅宋湯、煎肉餅、切沙拉之間快速切換;平行運算則是同時有好幾位廚師,每個人各自負責一道菜。

實務上有什麼差別?

多執行緒 重在便利與回應性。你使用多個執行緒讓程式不會「當住」:一個執行緒等待網路,另一個繪製介面,第三個在計算。整體感覺像是並行,但不一定同時。

平行運算 重在速度。多個處理器核心確實會同時執行任務的不同部分,以更快得到結果。

換句話說:多執行緒幫助你組織工作,而平行運算是用來加速它。

重要:
只要有彼此獨立、可同時進行的子任務,多執行緒幾乎隨時都有用。
當你想把工作實際分配到多個核心上以加速計算時,就需要平行運算。

範例:處理大型陣列

假設我們有一個包含 1,000 萬個數字的陣列,想要計算所有元素的總和。

序列式:
一個執行緒遍歷整個陣列並計算總和。簡單又可靠,但較慢。

多執行緒(但在單核心上):
你把陣列分成 4 份,建立 4 個執行緒,各自計算自己的部分。但如果只有一個核心,執行緒會輪流運作——不會有加速,切換的額外開銷甚至可能讓程式變慢。

平行(在多核心上):
你把陣列分成 4 份,啟動 4 個執行緒,且每個執行緒實際跑在不同核心上。最終總和由 4 個部分彙整而成。這會更快——尤其在資料量很大時。

不過,實作序列式處理陣列非常簡單,你已經多次寫過這類程式:

// 範例:序列式處理陣列
int[] arr = new int[10_000_000];
// ... 填入陣列資料 ...
long sum = 0;
for (int x : arr) {
    sum += x;
}
System.out.println(sum);

多執行緒和平行版本會稍微複雜一些,我們會在接下來的講次用現代工具來解析。

2. 為什麼需要平行運算

現代處理器早就不只一個核心。就連你的智慧型手機很可能至少有四個核心,而桌上型電腦與伺服器常見的是八核、十六核、三十二核甚至更多。若應用能使用所有這些核心,就能快上好幾倍。

過去處理器效能主要靠提升時脈——大約到 2000 年代中期都仍可行。但時脈提升受限於物理極限,於是進入多處理器與多核心的時代。如今,能有效把工作分配到各核心的程式才更有優勢。

哪些情況下,平行運算真的能加速?

  • 大數據處理: 日誌分析、統計、彙總——凡是能切成彼此獨立區塊的工作。
  • 渲染、影像與影片處理: 每個像素或片段都可以獨立處理。
  • 科學運算、模擬: 各種數學問題、模擬、模型訓練。
  • 伺服器端應用: 同時服務大量客戶端。
  • 反應式應用: 需要對大量事件快速回應,而不阻塞主執行緒。

什麼時候平行運算幫不上忙?

  • 當任務很小,啟動平行的額外開銷可能大於收益。
  • 當任務無法拆成獨立部分(例如每一步都依賴前一步)。
  • 當存在大量共享資源(例如同一個檔案)時,執行緒會互相干擾。

3. 平行運算的典型任務

來看看最常被「分配到多核心」的任務有哪些。

大規模數值計算

  • 在大型陣列上做加總、尋找最大/最小值、統計彙整。
  • 例如:計算一百萬個感測器的平均溫度。

集合處理

  • 對大型清單進行過濾、排序、轉換(例如處理網路商店的訂單)。
  • 例如:挑出所有價格高於 10,000 盧布的訂單,並依日期排序。

渲染與圖形處理

  • 替影像的所有像素套用濾鏡(例如轉為黑白)。
  • 每個像素都能獨立處理——平行運算的理想案例。

資料分析、Big Data

  • MapReduce、彙總、在巨量資料上計算統計。
  • 例如:處理一整年的日誌以找出異常。

範例:平行計算總和
假設我們有一個含 100 萬個數字的陣列。可以把它切成 4 份,分別在獨立的執行緒中計算每一份的總和,最後再把結果相加。

4. 平行運算的問題與挑戰

除錯困難
當程式在多個執行緒中運作,錯誤可能只在少數情況下出現,當執行緒以某種特殊方式「交會」時才會觸發。有時一千次才出現一次——非常難以重現與抓到。

資料競爭(race condition)
如果多個執行緒同時變更同一個變數或物件,可能得到不正確的結果。例如兩個執行緒同時增加計數器,最終值反而比預期小。

同步
為了避免競爭,必須同步存取共享資料——透過關鍵字 synchronized、各種鎖、原子變數與其他工具。這會讓程式碼更複雜,也可能導致其他問題(例如,deadlock——執行緒之間的相互鎖定)。

負載平衡
如果你把任務分成 4 份,但其中一份遠比其他份更重——三個執行緒早就做完在閒置,第四個還在跑。最後就沒有加速。

額外開銷
啟動執行緒、在它們之間切換、同步——這些都需要時間。如果任務很小,平行運算只會拖慢執行。

表格:方法比較

方法 什麼時候快 什麼時候會變慢 應用範例
序列式(1 執行緒) 小任務、邏輯簡單 大量資料 處理 10 行資料
多執行緒(在 1 核心上) 非同步任務(等待 IO CPU-bound(受限於處理器計算)任務,在單核心上 同時下載檔案
平行運算(多核心) 巨大且彼此獨立的任務 小任務、耦合度高 處理大型陣列

視覺化:看起來如何

// 序列式處理(1 執行緒)
[任務 1][任務 2][任務 3][任務 4]

// 在單核心上的多執行緒(靠切換邏輯)
[任務 1] [任務 2] [任務 3] [任務 4]
(實際上一次只會有一個在跑,其餘在等待)

// 四核心上的平行運算
[任務 1]    [任務 2]    [任務 3]    [任務 4]
(全部同時執行)

5. 嘗試平行運算時的常見錯誤

錯誤 1:什麼都想平行化。 許多新手以為:「執行緒越多就越快!」其實不然。如果任務很少或太過簡單——不會有任何收益,有時程式甚至會更慢。

錯誤 2:忽視同步。 若多個執行緒在沒有同步的情況下操作同一份資料——會出現資料競爭、邏輯損壞與難以捕捉的錯誤。

錯誤 3:為了平行而平行。 平行運算不是目的本身。只有在確實存在能有效拆成獨立部分的實際需求時才需要它。

錯誤 4:忽略任務特性。 有些任務根本無法平行化(例如第 N+1 步依賴第 N 步的結果)。在這種情況下,平行運算不會帶來優勢。

錯誤 5:忽略額外開銷。 啟動執行緒、切換、彙整結果——這些都需要時間。對小任務而言,這些時間可能比工作本身還多。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION