Python 的多執行緒受限於 GIL,無法有效利用多核心 CPU。實際應用中,即使多個執行緒同時運作,CPU 使用率也難以提升。相對地,多工處理能繞過 GIL 限制,透過建立多個獨立程式,充分發揮多核心效能。本文提供的程式碼範例展示瞭如何使用 multiprocessing 模組建立多個程式,執行相同的計算任務,並觀察 CPU 使用率的顯著提升。此外,也介紹了 concurrent.futures 模組,提供更簡潔的平行計算方式,適合 I/O 密集型任務。兩種方法各有優劣,開發者需根據實際應用場景選擇合適的方案。

多工處理與多執行緒:Python 中的 CPU 使用率比較

在 Python 中,多工處理和多執行緒是兩種不同的方法,用於利用多核心 CPU 的能力。然而,由於 CPython 的全域解譯鎖(GIL),多執行緒的效能受到限制。在本文中,我們將比較使用多執行緒和多工處理的差異,並探討如何使用 multiprocessing 模組來提高 CPU 使用率。

多執行緒的限制

使用多執行緒可以將工作負載分配給多個核心,但是由於 GIL 的限制,多執行緒無法完全發揮多核心 CPU 的能力。如前所述,使用多執行緒的程式只能使用到 116% 的 CPU 能力,即使有八個執行緒在平行執行。

使用多工處理

另一方面,使用多工處理可以建立新的程式,這些程式可以獨立執行,不受 GIL 的限制。Python 的 multiprocessing 模組提供了一個高層次的介面,用於建立新的程式。透過使用 multiprocessing 模組,我們可以重寫前面的例子,使用多工處理來提高 CPU 使用率。

示例程式碼

以下是使用 multiprocessing 模組的示例程式碼:

import random
import multiprocessing

def compute(results):
    results.append(sum([random.randint(1, 100) for i in range(1000000)]))

with multiprocessing.Manager() as manager:
    results = manager.list()
    workers = [multiprocessing.Process(target=compute, args=(results,)) for x in range(8)]
    for worker in workers:
        worker.start()
    for worker in workers:
        worker.join()

print("Results: %s" % results)

結果比較

使用多工處理的程式可以消耗到 332% 的 CPU 能力,比多執行緒的程式快了三倍。這是因為多工處理可以建立新的程式,這些程式可以獨立執行,不受 GIL 的限制。

使用多程式和並發期物實作平行計算

在進行多程式計算時,Python 的 multiprocessing 模組提供了一種方便的方式來實作平行計算。下面是一個使用 multiprocessing 的例子:

import multiprocessing
import random

def compute(n):
    return sum(random.randint(1, 100) for i in range(1000000))

if __name__ == '__main__':
    # 啟動 8 個工作者
    pool = multiprocessing.Pool(processes=8)
    print("結果:%s" % pool.map(compute, range(8)))

在這個例子中,我們定義了一個 compute 函式,該函式計算了一個隨機數字的總和。然後,我們啟動了 8 個工作者,並使用 pool.map 函式將 compute 函式應用於範圍為 8 的數字列表上。

使用 multiprocessing 的優點是,它可以自動管理工作者程式的建立和終止,減少了手動管理程式的複雜性。

使用並發期物實作平行計算

Python 3.2 引入了 concurrent.futures 模組,該模組提供了一種簡單的方式來實作平行計算。下面是一個使用 concurrent.futures 的例子:

from concurrent import futures
import random

def compute():
    return sum(random.randint(1, 100) for i in range(1000000))

with futures.ThreadPoolExecutor(max_workers=8) as executor:
    futs = [executor.submit(compute) for _ in range(8)]
    results = [f.result() for f in futs]
    print("結果:%s" % results)

在這個例子中,我們定義了一個 compute 函式,該函式計算了一個隨機數字的總和。然後,我們建立了一個 ThreadPoolExecutor 物件,並提交 8 個任務到執行緒池中。最後,我們收集了每個任務的結果,並列印預出來。

使用 concurrent.futures 的優點是,它提供了一種簡單的方式來實作平行計算,同時也提供了對執行緒池的控制,減少了手動管理執行緒的複雜性。

比較兩種方法

兩種方法都可以實作平行計算,但是 concurrent.futures 的優點是,它提供了一種簡單的方式來實作平行計算,同時也提供了對執行緒池的控制。另一方面,multiprocessing 的優點是,它可以自動管理工作者程式的建立和終止,減少了手動管理程式的複雜性。

圖表翻譯:

  flowchart TD
    A[開始] --> B[選擇方法]
    B --> C[multiprocessing]
    B --> D[concurrent.futures]
    C --> E[啟動工作者]
    D --> F[提交任務]
    E --> G[收集結果]
    F --> G
    G --> H[列印結果]

內容解密:

在這個流程圖中,我們首先選擇實作平行計算的方法,可以是 multiprocessingconcurrent.futures。如果選擇 multiprocessing,我們啟動工作者程式並提交任務。如果選擇 concurrent.futures,我們提交任務到執行緒池中。最後,我們收集每個任務的結果並列印預出來。

從效能最佳化視角來看,Python 的多工處理和多執行緒在 CPU 使用率方面表現出明顯差異。深入剖析 GIL 的影響後,可以發現多執行緒在 CPU 密集型任務中受限於單一核心執行,即使模擬多執行緒也僅能達到有限的 CPU 使用率提升。對比之下,多工處理藉由建立多個獨立程式,有效繞過 GIL 的限制,實作真正的平行計算,從而大幅提高 CPU 使用率,充分利用多核心架構的優勢。實務上,對於 CPU 密集型任務,技術團隊應優先考慮 multiprocessing 模組,並搭配適當的程式間通訊機制,例如 ManagerQueue,以最佳化資料交換和同步。然而,多工處理也引入了程式管理的額外開銷,因此並非所有情況都適用。對於 I/O 密集型任務,多執行緒或非同步程式設計模型可能更為有效。展望未來,隨著 Python 生態的持續發展,預計會有更多針對 GIL 的最佳化方案出現,例如更細粒度的鎖機制或替代的執行模型,進一步提升 Python 在多核心環境下的效能。玄貓認為,開發者應根據具體應用場景和效能需求,審慎選擇多工處理或多執行緒,並持續關注 Python 的發展趨勢,以做出最佳的技術決策。