我們知道現在硬件飛速發展,多核CPU 成了標配。為了提高程序的效率,一個方面改變程序的順序執行,用異步方式,防止由于某個耗時步驟,而影響后續程序的執行。另一個方面是采用并發方式執行,重復利用多核CPU優勢加速執行。關于并發編程大家可能比較熟悉的是Golang的協程、通道和Node.js 的async.parallel異步并發編程。就并發編程來說,Python/ target=_blank class=infotextkey>Python不是一門合適的語言,主要是Python有一個解析器(CPython)內置的全局解釋鎖GIL。 GIL限制Python中一次只能有一個線程訪問Python對象,從而我們無法實現多線程分配到多個CPU執行,這是一個極大限制,限制Python并發編程。當然限制歸限制,Python標準庫中都已經引入了多進程和多線程庫,所以Python并發程序相當簡單。
本文中,蟲蟲給大家實例介紹一下Python的并發編程
并發編程
關于python并發編程,我們推薦優雅地創建并發程序三部曲:
首先,編寫一個按順序執行任務的腳本。
其次,腳本中的執行程序(耗時任務)提取為一個執行函數,并使用map函數調用。
最后,使用并發模塊中的函數替換map即可。
實例腳本
該實例中,我們用到一個小的圖片爬蟲,使用urllib從Picsum網站下載20張圖片,具體腳本程序如下:
import urllib.request import time url = 'https://picsum.photos/id/{}/200/300' args = [(n, url.format(n)) for n in range(20)] start = time.time() for pic_id, url in args: res = urllib.request.urlopen(url) pic = res.read() with open(f'./{pic_id}.jpg', 'wb') as f: f.write(pic) print(f'圖片 {pic_id} 已經保存!') end = time.time() msg ='共耗時 {:.3f} 秒下載完成。' print(msg.format(end-start)
python pic_get.py 運行該腳本,結果如下:
圖片 0 已經保存! 圖片 1 已經保存! 圖片 2 已經保存! ... 共耗時 26.694 秒下載完成。
下載共耗費不到半分鐘,接著按照我們優雅的三部曲,改造這個腳本。
使用Map改造腳本
下面腳本中,我們將下載圖片的代碼打包到一個執行函數get_img中。
import urllib.request import time def get_img(pic_id, url): res = urllib.request.urlopen(url) pic = res.read() with open(f'test/{pic_id}.jpg', 'wb') as f: f.write(pic) print(f'圖片 {pic_id} 已經保存!') def main(): url = 'https://picsum.photos/id/{}/200/300' pic_ids = [i for i in range(20)] ; urls=[(url.format(n)) for n in range(20)] start = time.time() for _ in map(get_img, pic_ids, urls): pass end = time.time() msg = '共耗時{:.3f}秒下載完成。' print(msg.format(end-start)) if __name__ == '__main__': main()
上述腳本中,用map函數替換先前腳本中的for循環(黑體部分)。map是一個函數式編程語法,該函數會生成一個迭代器,迭代器會執行迭代調用get_img()。關于map()函數熟悉函數式編程人可能會覺得有點奇怪,請自己搜索資料充電,此處,我們用它來充當并發編程網關。
圖片 0 已經保存! 圖片 1 已經保存! 圖片 2 已經保存! ... 圖片 19 已經保存! 共耗時26.023秒下載完成。
用map改造后,運行腳本總耗時大體上和腳本一致。
多線程并發處理
Python標準庫的current.futures模塊包含了大量并發編程的包裝函數,詳細說明,可參見官方文檔,此處我們直接上代碼。
將pic_get1.py中的程序做簡單改進,就能實現多線程腳本:
首先在腳本開頭引入多線程函數:
from concurrent.futures import ThreadPoolExecutor
接著替換
for _ in map(get_img, pic_ids, urls): pass
為
with ThreadPoolExecutor(max_workers=20) as do: do.map(get_img, pic_ids, urls)
即可。執行結果:
圖片 0 已經保存! 圖片 2 已經保存! 圖片 5 已經保存! ... 圖片 9 已經保存! 共耗時2.913秒下載完成。
總耗時由26秒,減少到了大約3秒。大概快了8倍。并發執行的效果還是杠杠的。
程序中我們使用with ThreadPoolExecutor語句產生一個執行器do。通過將get_img和相應的參數映射到執行程序,自動生成多線程執行。
大家可能注意到了在多線程腳本執行后,圖片下載時候不是以前的0~19的順序的,而是不同線程并發執行的所以完成提示信息也是亂序的。
多進程處理
多進程的改造也非常簡單,我么只需把之前多線程腳本中的ThreadPoolExecutor替換為ProcessPoolExecutor即可。
from concurrent.futures import ProcessPoolExecutor
...
with ProcessPoolExecutor(max_workers=20) as do: do.map(get_img, pic_ids, urls)
執行結果:
圖片 9 已經保存! 圖片 6 已經保存! ... 圖片 11 已經保存! 圖片 15 已經保存! 共耗時4.606秒下載完成
也非常快了,4秒鐘就完成了,但是比多線程的3秒,稍微慢點。為什么多進程要比多線程慢呢?顧名思義,多進程程序會啟用多個進程,而多線程會使用線程。Python中一個進程可以運行多個線程。每個進程都有其適當的Python解釋器和適當的GIL。相比較而已,啟動一個進程是更加耗時,重的操作,所以需要花費的時間更多。
斐波那契數列計算
為了進一步說明Python中線程和進程之間的區別,我們再來舉一個大量計算的例子,斐波那契數列的計算。
根據斐波那契數列的定義我們用遞歸方法編寫實現其計算:
def fib(n): if n == 1: return 0 elif n == 2: return 1 else: return fib(n-1) + fib(n-2)
在不使用numpy的情況下用普通Python計算比較慢:
def main(): fib_range = list(range(1, 35)) times = [] for run in range(10): start = time.time() for n in fib_range: fib(n) end = time.time() times.Append(end-start) print('波那契數列fib(35)計算平均耗時 {:.3f}。'.format(np.mean(times))
結果:
波那契數列fib(35)計算平均耗時 5.200
下面我們試著用并發計算來加速計算。
讓我們通過線程加速它!為此,我用受信任的ThreadPoolExecutor替換for循環,如下所示:
with ProcessPoolExecutor() as do: do.map(fib, fib_range)
執行結果:
波那契數列fib(35)計算平均耗時 5.239。
什么?加速后,反而慢,好像多線程沒起到作用。這就是GIL的因素導致的,盡管使用了多個線程,生成了一堆線程,但是這些線程都在同一進程中運行并共享一個GIL。所以斐波那契序列盡管是并發計算的,這些線程在只能在一個CPU上循序執行。
進程可以分布在不同的CPU核心,而在同一進程上運行的線程則不能。使CPU消耗最大的操作為CPU綁定操作。為了加快CPU限制的操作,應該啟動多個進程計算。我們用ProcessPoolExecutor替換ThreadPoolExecutor再試試:
波那契數列fib(35)計算平均耗時 3.591
性能提高了一點。
除了并發的方式外,我們可以用算法優化方法來提高性能,在數值計算中,這是一種更有效的方法,比如,我們改造fib函數:
def fib(n): a, b, i = 0, 1, 1 while i < n: a, b = b, a + b i += 1 return b
上述方法中,巧妙用內存存中的變量歷史迭代的前兩次結果都存在內存中,所以該次計算中無需回溯迭代計算,這樣計算效率O(1),基本上可以秒出結果。
使用新算法后的執行結果:
波那契數列fib(35)計算平均耗時 0.000。
總結
本文我們實例介紹了Python中的并發編程,關于并發編程由于標準庫中給我們打包好了方便使用的并發函數使得其使用非常方便。需要注意的是Python中的并發不管是多線程在IO操作中是有效的,而在其他方面,如數值結算時候就受GIL限制無用了。關于并發計算和GIL有心的話,可以參考有關文檔進一步深入學習了解。