Python 並行任務技巧

FROM:    http://segmentfault.com/a/1190000000382873

 

Python的併發處理能力臭名昭著。先撇開線程以及GIL方面的問題不說,我覺得多線程問題的根源不在技術上而在於理念。大部分關於Pyhon線程和多進程的資料雖然都很不錯,但卻過於細節。這些資料講的都是虎頭蛇尾,到了真正實際使用的部分卻草草結束了。

傳統例子

在DDG https://duckduckgo.com/ 搜索「Python threading tutorial」關鍵字,結果基本上卻都是相同的類+隊列的示例。
標準線程多進程,生產者/消費者示例:

請輸入圖片描述

這裏是代碼截圖,如果用其他模式貼出大段代碼會很不美觀。文本模式點這裏 here
Mmm.. 感覺像是java代碼
在此我不想印證採用生產者/消費者模式來處理線程/多進程是錯誤的— 確實沒問題。實際上這也是解決很多問題的最佳選擇。但是,我卻不認爲這是日常工作中常用的方式。

問題所在

一開始,你需要一個執行下面操作的鋪墊類。接着,你需要創建一個傳遞對象的隊列,並在隊列兩端實時監聽以完成任務。(很有可能需要兩個隊列互相通信或者存儲數據)
Worker越多,問題越大.
下一步,你可能會考慮把這些worker放入一個線程池一邊提高Python的處理速度。下面是
IBM tutorial 上關於線程較好的示例代碼。這是大家常用到的利用多線程處理web頁面的場景

請輸入圖片描述

Seriously, Medium. Fix your code support. Code is Here.

感覺效果應該很好,但是看看這些代碼!初始化方法、線程跟蹤,最糟的是,如果你也和我一樣是個容易犯死鎖問題的人,這裏的join語句就要出錯了。這樣就開始變得更加複雜了!
到現在爲止都做了些什麼?基本上沒什麼。上面的代碼都是些基礎功能,而且很容易出錯。(天啊,我忘了寫上在隊列對象上調用task_done()方法(我懶得修復這個問題在重新截圖)),這真是性價比太低。所幸的是,我們有更好的辦法.

引入:Map

Map 是個很酷的小功能,也是簡化Python併發代碼的關鍵。對那些不太熟悉Map的來說,它有點類似Lisp.它就是序列化的功能映射功能. e.g.

urls = [', ']
results = map(urllib2.urlopen, urls)

這裏調用urlopen方法,並把之前的調用結果全都返回並按順序存儲到一個集合中。這有點類似

results = []
for url in urls: 
    results.append(urllib2.urlopen(url))

Map能夠處理集合按順序遍歷,最終將調用產生的結果保存在一個簡單的集合當中。
爲什麼要提到它?因爲在引入需要的包文件後,Map能大大簡化併發的複雜度!

請輸入圖片描述

支持Map併發的包文件有兩個:
Multiprocessing,還有少爲人知的但卻功能強大的子文件 multiprocessing.dummy. .

Digression這是啥東西?沒聽說過線程引用叫dummy的多進程包文件。我也是直到最近才知道。它在多進程的說明文檔中也只被提到了一句。它的效果也只是讓大家直到有這麼個東西而已。這可真是營銷的失誤!

Dummy是一個多進程包的完整拷貝。唯一不同的是,多進程包使用進程,而dummy使用線程(自然也有Python本身的一些限制)。所以一個有的另一個也有。這樣在兩種模式間切換就十分簡單,並且在判斷框架調用時使用的是IO還是CPU模式非常有幫助。

準備開始

準備使用帶有併發的map功能首先要導入相關包文件:

from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool

然後初始化:

pool = ThreadPool()

就這麼簡單一句解決了example2.py中build_worker_pool的功能. 具體來講,它首先創建一些有效的worker啓動它並將其保存在一些變量中以便隨時訪問。
pool對象需要一些參數,但現在最緊要的就是:進程。它可以限定線程池中worker的數量。如果不填,它將採用系統的內核數作爲初值。

一般情況下,如果你進行的是計算密集型多進程任務,內核越多意味着速度越快(當然這是有前提的)。但如果是涉及到網絡計算方面,影響的因素就千差萬別。所以最好還是能給出合適的線程池大小數。

pool = ThreadPool(4) # Sets the pool size to 4

如果運行的線程很多,頻繁的切換線程會十分影響工作效率。所以最好還是能通過調試找出任務調度的時間平衡點。
好的,既然已經建好了線程池對象還有那些簡單的併發內容。咱們就來重寫一些example2.py中的url opener吧!

請輸入圖片描述

看吧!只用4行代碼就搞定了!其中三行還是固定寫法。使用map方法簡單的搞定了之前需要40行代碼做的事!爲了增加趣味性,我分別統計了不同線程池大小的運行時間。

請輸入圖片描述

結果:

請輸入圖片描述

效果驚人!看來調試一下確實很有用。當線程池大小超過9以後,在我本機上的運行效果已相差無幾。

示例 2:

生成上千張圖像的縮略圖:
現在咱們看一年計算密集型的任務!我最常遇到的這類問題之一就是大量圖像文件夾的處理。
其中一項任務就是創建縮略圖。這也是併發中比較成熟的一項功能了。
基礎單線程創建過程

請輸入圖片描述

作爲示例來說稍微有點複雜。但其實就是傳一個文件夾目錄進來,獲取到裏面所有的圖片,分別創建好縮略圖然後保存到各自的目錄當中。
在我的電腦上,處理大約6000張圖片大約耗時27.9秒.
如果使用併發map處理替代其中的for循環:
請輸入圖片描述

只用了5.6 秒!

就改了幾行代碼速度卻能得到如此巨大的提升。最終版本的處理速度還要更快。因爲我們將計算密集型與IO密集型任務分派到各自獨立的線程和進程當中,這也許會容易造成死鎖,但相對於map強勁的功能,通過簡單的調試我們最終總能設計出優美、高可靠性的程序。就現在而言,也別無它法。好了。來感受一下一行代碼的併發程序吧。