Python實現多進程:Pool類詳解

1 引言

multiprocessing是一個用於產生多進程的包,既可以實現本地的多進程,也可以實現遠程的多進程。通過使用多個子進程,從而可以繞開Python的全局解釋器鎖(GIL)。通過使用多進程,我們可以充分利用機器的運算能力。本文將介紹這個包中的Pool類。

2 Pool類

一個Pool對象控制着一個進程池,這個進程池中有很多可執行任務的進程。

class multiprocessing.pool.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])

processes是將要使用的進程數。一般我們會先用cpu_count()方法來確定本地有多少個核,然後再決定使用幾個核來跑程序,默認取0,這個時候就是使用本地所有的核來跑程序。

maxtasksperchild:工作進程退出之前可以完成的任務數,完成後用一個新的工作進程來替代原進程,來讓閒置的資源被釋放。maxtasksperchild默認是None,意味着只要Pool存在工作進程就會一直存活。下面是官方文檔中關於這個參數的解釋:

通常來說,Pool 中的 Worker 進程的生命週期和進程池的工作隊列一樣長。一些其他系統中(如 Apache, mod_wsgi 等)也可以發現另一種模式,他們會讓工作進程在完成一些任務後退出,清理、釋放資源,然後啓動一個新的進程代替舊的工作進程。 Pool 的 maxtasksperchild 參數給用戶提供了這種能力。

一般情況下我們都是隻設置processes這一個參數。

2.1 Pool類中的方法列表

2.1.1 五種分派任務的方法
  1. apply()
  2. apply_async()
  3. map()
  4. map_async()
  5. imap()
2.1.2 三種管理進程池的方法:
  1. close()
  2. terminate()
  3. join()

2.2 apply(func[, args[, kwds]])方法

apply(func[, args[, kwds]])這個方法只能將一個任務分配個一個進程,如果想要多任務並行,就需要多次調用。

from multiprocessing import Pool, cpu_count
import time

print('CPU核的數量:',cpu_count())

def func1(x1):
    time.sleep(0.1)
    return (x1,x1*x1)

def func2(x1,x2):
    time.sleep(0.1)
    return (x1,x2,x1+x2)
    
begin = time.time()

with Pool(10) as p:
    for i in range(10):
        result = p.apply(func2,(1,i,))
    p.close()
    p.join()
    
during = time.time()-begin

print(during)

輸出爲:

CPU核的數量: 12
1.1421127319335938

可以看到程序運行了1.14秒,而不調用多進程的話這個程序這個程序只需要執行1.01秒。實際上apply()方法是阻塞的,也就是說如果當前的進程沒有執行完,就需要等待執行完後再執行後續進程。

import time
def func2(x1,x2):
    time.sleep(0.1)
    return (x1,x2,x1+x2)

begin = time.time()

for i in range(10):
    result = func2(1,i)
    
during = time.time()-begin

print(during)
1.0019781589508057

2.3 apply_async(func[, args[, kwds[, callback[, error_callback]]]])方法

這個方法是非阻塞的,它是apply()方法的一個變種,返回的是一個對象。所以你就需要先將這些對象保存在列表中,在主進程結束之後再從這些對象中獲取結果。從執行時間來看,apply_async實現了並行執行。

pool.apply_async之後的語句都是阻塞執行的,調用 result.get() 會等待上一個任務執行完之後纔會分配下一個任務。事實上,獲取返回值的過程最好放在進程池回收之後進行,避免阻塞後面的語句。

from multiprocessing import Pool, cpu_count
import time

print('CPU核的數量:',cpu_count())

def func1(x1):
    time.sleep(0.1)
    return (x1,x1*x1)

def func2(x1,x2):
    time.sleep(0.1)
    return (x1,x2,x1+x2)

results = []

begin = time.time()

with Pool(10) as p:
    for i in range(10):
        result = p.apply_async(func2,(1,i,))
        results.append(result)
        #rint(result)
    p.close()
    p.join()
    
during = time.time()-begin
print(results[3].get())
print(during)
CPU核的數量: 12
(1, 3, 4)
0.23436999320983887
2.4 map(func, iterable[, chunksize])

這個map函數只支持一個可迭代參數,它的多個參數迭代版本是starmap()。它會保持阻塞直到獲得結果。具體而言這個方法很耗內存,推薦使用imap()

starmap(func, iterable[, chunksize])和 map() 類似,不過 iterable 中的每一項會被解包再作爲函數參數。比如可迭代對象 [(1,2), (3, 4)] 會轉化爲等價於 [func(1,2), func(3,4)] 的調用。

2.5 map_async(func, iterable[, chunksize[, callback[, error_callback]]])

map()返回的是一個列表,這個方法返回的是一個對象,方法的功能同上。

2.6 imap(func, iterable[, chunksize])

map()方法的lazy版本。這個方法會將可迭代對象分割爲許多塊,然後提交給進程池。可以將 chunksize 設置爲一個正整數從而(近似)指定每個塊的大小可以。它支持多參數

from multiprocessing import Pool, cpu_count
import time

print('CPU核的數量:',cpu_count())

def func1(x1):
    time.sleep(0.1)
    return (x1,x1*x1)

def func2(x1,x2):
    time.sleep(0.1)
    return (x1,x2,x1+x2)


begin = time.time()

with Pool(10) as p:
    result = p.imap(func1,[(1,i) for i in range(10)])
    
    p.close()
    p.join()
    
during = time.time()-begin

print(during)

關於其中的chunksize參數,找到的一個解釋是:

看看documentation for Pool.map似乎你幾乎是正確的:chunksize參數將導致iterable被分割成大約相同大小的片段,並且每個片段作爲單獨的任務提交.因此,在您的示例中,是的,map將採用前10個(大約),將其作爲單個處理器的任務提交…然後將下一個10作爲另一個任務提交,依此類推.請注意,這並不意味着這將使處理器每10個文件交替一次,處理器#1很可能最終得到1-10和11-20,處理器#2得到21-30和31-40.

但在我實際的測評時發現chunksize實際對運行速度沒啥影響,但文檔中說chunksize越大,速度越大。

from multiprocessing import Pool, cpu_count
import time

print('CPU核的數量:',cpu_count())

def func1(x1):
    time.sleep(0.01)
    return (x1,x1*x1)

def func2(x1,x2):
    time.sleep(0.01)
    return (x1,x2,x1+x2)


begin = time.time()

with Pool(5) as p:
    result = p.imap(func1,range(1000),chunksize = 1)
    print(result)
    p.close()
    p.join()
    
during = time.time()-begin

print(during)
#[result.next() for r in range(100)]
chunksize 1 2 3
時間 2.126403331756592 2.12638783454895 2.13116717338562

2.3 close(),join()和terminate()

close(): 阻止後續任務提交到進程池,當所有任務執行完成後,工作進程會退出。

terminate(): 不必等待未完成的任務,立即停止工作進程。當進程池對象被垃圾回收時, terminate() 會立即調用。

join(): 等待工作進程結束。調用 join() 前必須先調用 close() 或者 terminate() 。

發佈了25 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章