Python 並行任務技巧

發表於2015-07-08

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.

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

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

支援Map併發的包檔案有兩個:
Multiprocessing,還有少為人知的但卻功能強大的子檔案 multiprocessing.dummy. .

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

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

 

準備開始

準備使用帶有併發的map功能首先要匯入相關包檔案:

然後初始化:

就這麼簡單一句解決了example2.py中build_worker_pool的功能. 具體來講,它首先建立一些有效的worker啟動它並將其儲存在一些變數中以便隨時訪問。
pool物件需要一些引數,但現在最緊要的就是:程式。它可以限定執行緒池中worker的數量。如果不填,它將採用系統的核心數作為初值。

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

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

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

 

結果:

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

示例 2:

生成上千張影象的縮圖:

現在我們們看一年計算密集型的任務!我最常遇到的這類問題之一就是大量影象資料夾的處理。

其中一項任務就是建立縮圖。這也是併發中比較成熟的一項功能了。

基礎單執行緒建立過程

作為示例來說稍微有點複雜。但其實就是傳一個資料夾目錄進來,獲取到裡面所有的圖片,分別建立好縮圖然後儲存到各自的目錄當中。

在我的電腦上,處理大約6000張圖片大約耗時27.9秒.

如果使用併發map處理替代其中的for迴圈:

只用了5.6 秒!

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

相關文章