之前學習了多線程以及線程池,他們在執(zhí)行I/O密集的程序的時候,性能是很高的,但是如果我們有大量的CPU密集型工作的程序,現在想利用多個CPU的優(yōu)勢運行的更快,應該怎么解決呢?
這時候,就不能使用多線程了,而是需要真正的并行來解決問題。
在concurrent.futures庫中提供了一個ProcessPoolExecutor類,可用來在單獨運行的python解釋器實例中執(zhí)行計算密集的函數。
ProcessPoolExecutor的典型用法是下面這樣的:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
"""
在進程池pool中并行執(zhí)行任務
"""
在底層,ProcessPoolExecutor創(chuàng)建了N個獨立運行的Python解釋器,這里的N就是系統(tǒng)上檢測到的可用的CPU個數??梢詣?chuàng)建和修改Python的進程數,只要給ProcessPoolExecutor(N)提供一個可選的參數。進程池會一直運行,直到with語句塊中的最后一條語句執(zhí)行完畢為止,此時進程池就會關閉。但是程序會一直等待所有已經提交的任務都處理完畢為止。
提交到進程池中的任務必須定義為函數形式。有兩種方法可以提交任務。如果想并行處理一個列表推導式或者map()操作,可以使用pool.map():
from concurrent.futures import ProcessPoolExecutor
def work(x):
"""任務邏輯"""
return x
data = [1, 2, 3, 4]
with ProcessPoolExecutor() as pool:
"""
在進程池pool中并行執(zhí)行任務
"""
results = pool.map(work, data)
另一種方式是通過pool.submit()來手動提交單獨的任務:
from concurrent.futures import ProcessPoolExecutor
def work(x):
"""任務邏輯"""
return x
data = 1
with ProcessPoolExecutor() as pool:
"""
在進程池pool中并行執(zhí)行任務
"""
future_result = pool.submit(work, data)
result = future_result.result()
如果手動提交任務,得到的結果就是一個Future實例。要獲取到結果還需要手動調用result()方法。這么做會阻塞進程,直到結果返回為止。所以與其讓進程阻塞,不如提供一個回調函數,讓他執(zhí)行任務完成時出發(fā)執(zhí)行。示例如下:
from concurrent.futures import ProcessPoolExecutor
def work(x):
"""任務邏輯"""
return x
def when_done(r):
print("result:", r.result())
data = 1
with ProcessPoolExecutor() as pool:
"""
在進程池pool中并行執(zhí)行任務
"""
future_result = pool.submit(work, data)
future_result.add_done_callback(when_done)
用戶提供的回調函數需要接受一個Future實例,必須用他才能獲取實際的結果。
盡管進程池看起來很簡單,但是在設計規(guī)模更大的程序時有下面幾個重要的因素需要考慮。
- 這種并行處理的技術只適用于可以將問題分解成各個獨立部分的情況。
- 任務必須定義成普通函數來提交。實例方法,比包或者其他類型的可調用對象都是不支持并行處理的。
- 函數的參數和返回值必須可兼容
pickle編碼。任務的執(zhí)行是在單獨的解釋器進程中完成的。這中間需要進程間通信。因此,在不同的解釋器之間交換數據必須要進行序列化處理。 - 提交的工作函數都不應該維護持久的狀態(tài)或者帶有副作用。除了簡單的日志功能,一旦子進程啟動,將無法控制他的行為。因此,為了讓思路保持清晰,最好讓每件事情都保持簡單,讓任務在不會修改執(zhí)行環(huán)境的純函數中執(zhí)行。
- 進程池是通過調用UNIX上的fork()系統(tǒng)調用來創(chuàng)建的。這么做會克隆一個Python解釋器,在fork()時會包含所有的程序狀態(tài)。在windows上,這么做會加載一個獨立的解釋器拷貝,但不包含狀態(tài)??寺〕鰜淼倪M程在首次調用
pool.map()或者pool.submit()之前不會實際運行。 - 既然是克隆一個獨立的解釋器,那每個進程都可以再執(zhí)行線程。當進程池和多線程技術結合在一起使用的時候需要格外的小心。特別是,很可能我們應該在創(chuàng)建任何線程之前優(yōu)先創(chuàng)建并加載進程池(例如,當程序啟動時在主線程中創(chuàng)建進程池)。
本文最先發(fā)布于:SavingUnhappy