使用concurrent.futures模塊啟動進程
concurrent.futures 模塊的文檔
(https://docs.python.org/3/library/concurrent.futures.html)副標題
是“Launching parallel tasks”(執行并行任務)。這個模塊實現的是真正
的并行計算,因為它使用 ProcessPoolExecutor 類把工作分配給多個
Python 進程處理。因此,如果需要做 CPU 密集型處理,使用這個模塊
能繞開 GIL,利用所有可用的 CPU 核心。
ProcessPoolExecutor 和 ThreadPoolExecutor 類都實現了通用的
Executor 接口,因此使用 concurrent.futures 模塊能特別輕松地把
基于線程的方案轉成基于進程的方案。
下載國旗的示例或其他 I/O 密集型作業使用 ProcessPoolExecutor 類
得不到任何好處。這一點易于驗證,只需把示例 17-3 中下面這幾行:
def download_many(cc_list):workers = min(MAX_WORKERS, len(cc_list))with futures.ThreadPoolExecutor(workers) as executor:
改成:
def download_many(cc_list):with futures.ProcessPoolExecutor() as executor:
對簡單的用途來說,這兩個實現 Executor 接口的類唯一值得注意的區
別是,ThreadPoolExecutor.__init__
方法需要 max_workers 參
數,指定線程池中線程的數量。在 ProcessPoolExecutor 類中,那個
參數是可選的,而且大多數情況下不使用——默認值是
os.cpu_count() 函數返回的 CPU 數量。這樣處理說得通,因為對
CPU 密集型的處理來說,不可能要求使用超過 CPU 數量的職程。而對
I/O 密集型處理來說,可以在一個 ThreadPoolExecutor 實例中使用 10個、100 個或 1000 個線程;最佳線程數取決于做的是什么事,以及可
用內存有多少,因此要仔細測試才能找到最佳的線程數。
經過幾次測試,我發現使用 ProcessPoolExecutor 實例下載 20 面國
旗的時間增加到了 1.8 秒,而原來使用 ThreadPoolExecutor 的版本是
1.4 秒。主要原因可能是,我的電腦用的是四核 CPU,因此限制只能有
4 個并發下載,而使用線程池的版本有 20 個工作的線程。
ProcessPoolExecutor 的價值體現在 CPU 密集型作業上。我用兩個
CPU 密集型腳本做了一些性能測試。
arcfour_futures.py
這個腳本(代碼清單參見示例 A-7)純粹使用 Python 實現 RC4 算
法。我加密并解密了 12 個字節數組,大小從 149KB 到 384KB 不等。
sha_futures.py
這個腳本(代碼清單參見示例 A-9)使用標準庫中的 hashlib 模塊
(使用 OpenSSL 庫實現)實現 SHA-256 算法。我計算了 12 個 1MB 字
節數組的 SHA-256 散列值。
這兩個腳本除了顯示匯總結果之外,沒有使用 I/O。構建和處理數據的
過程都在內存中完成,因此 I/O 對執行時間沒有影響。
我運行了 64 次 RC4 示例,48 次 SHA 示例,平均時間如表 17-1 所示。
統計的時間中包含派生工作進程的時間。
表17-1:在配有Intel Core i7 2.7 GHz四核CPU的設備中,使用Python
3.4運行RC4和SHA示例,分別使用1~4個職程得到的時間和提速倍數可以看出,對加密算法來說,使用 ProcessPoolExecutor 類派生 4 個
工作的進程后(如果有 4 個 CPU 核心的話),性能可以提高兩倍。
對那個純粹使用 Python 實現的 RC4 示例來說,如果使用 PyPy 和 4 個職
程,與使用 CPython 和 4 個職程相比,速度能提高 3.8 倍。以表 17-1 中
使用 CPython 和一個職程的運行時間為基準,速度提升了 7.8 倍。
如果使用 Python 處理 CPU 密集型工作,應該試試
PyPy(http://pypy.org)。使用 PyPy 運行 arcfour_futures.py 腳本,速
度快了 3.8~5.1 倍;具體的倍數由職程的數量決定。我測試時使用
的是 PyPy 2.4.0,這一版與 Python 3.2.5 兼容,因此標準庫中有
concurrent.futures 模塊。
下面通過一個演示程序來研究線程池的行為。這個程序會創建一個包含
3 個職程的線程池,運行 5 個可調用的對象,輸出帶有時間戳的消息。