天天看點

快速掌握用python寫并行程式,幹貨滿滿

目錄

2.2 改用GPU處理計算密集型程式

三、用python寫并行程式

四、multiprocessing實戰

小子今天想來談談“并行計算”,作為一個非科班人員,我為什麼去搗鼓這麼一個在科班裡也比較專業的問題了。這就要說下我前幾天做的一個作業了,當時我用python寫了個程式,結果運作了一天,這個速度可讓我愁了,我還怎麼優化,怎麼交作業啊。于是小子就去各大論壇尋丹問藥了,終于讓我發現可以用并行計算來最大化壓榨電腦的CPU,提升計算效率,而且python裡有multiprocessing這個庫可以提供并行計算接口,于是小子花1天時間改程序式,終于在規定時間内做出了自己滿意的結果,上交了作業。之後,小子對并行計算充滿了興趣,于是又重新在Google上遊曆了一番,大緻弄清了GPU、CPU、程序、線程、并行計算、分布式計算等概念,也把python的multiprocessing耍了一遍,現在小子也算略有心得了,是以來此立碑,以示後來遊客。

小子本文分為四部分,一是大資料時代現狀,其二是面對挑戰的方法,然後是用python寫并行程式,最後是multiprocessing實戰。

一、大資料時代的現狀

目前我們正處于大資料時代,每天我們會通過手機、電腦等裝置不斷的将自己的資料傳到網際網路上。據統計,YouTube上每分鐘就會增加500多小時的視訊,面對如此海量的資料,如何高效的存儲與處理它們就成了目前最大的挑戰。

但在這個對硬體要求越來越高的時代,CPU卻似乎并不這麼給力了。自2013年以來,處理器頻率的增長速度逐漸放緩了,目前CPU的頻率主要分布在3~4GHz。這個也是可以了解的,畢竟摩爾定律都生效了50年了,如果它老人家還如此給力,那我們以後就隻要靜等處理器頻率提升,什麼計算問題在未來那都不是話下了。實際上CPU與頻率是于能耗密切相關的,我們之前可以通過加電壓來提升頻率,但當能耗太大,散熱問題就無法解決了,是以頻率就逐漸穩定下來了,而Intel與AMD等大制造商也将目标轉向了多核晶片,目前普通桌面PC也達到了4~8核。

二、面對挑戰的方法

咱們有了多核CPU,以及大量計算裝置,那我們怎麼來用它們應對大資料時代的挑戰了。那就要提到下面的方法了。

2.1 并行計算

并行(parallelism)是指程式運作時的狀态,如果在同時刻有多個“工作機關”運作,則所運作的程式處于并行狀态。圖一是并行程式的示例,開始并行後,程式從主線程分出許多小的線程并同步執行,此時每個線程在各個獨立的CPU進行運作,在所有線程都運作完成之後,它們會重新合并為主線程,而運作結果也會進行合并,并交給主線程繼續處理。

快速掌握用python寫并行程式,幹貨滿滿

圖一、多線程并行

圖二是一個多線程的任務(沿線為線程時間),但它不是并行任務。這是因為task1與task2總是不在同一時刻執行,這個情況下單核CPU完全可以同時執行task1與task2。方法是在task1不執行的時候立即将CPU資源給task2用,task2空閑的時候CPU給task1用,這樣通過時間窗調整任務,即可實作多線程程式,但task1與task2并沒有同時執行過,是以不能稱為并行。我們可以稱它為并發(concurrency)程式,這個程式一定意義上提升了單個CPU的使用率,是以效率也相對較高。

快速掌握用python寫并行程式,幹貨滿滿

圖二、多線程并發

并行程式設計模型:

資料并行(Data Parallel)模型:将相同的操作同時作用于不同資料,隻需要簡單地指明執行什麼并行操作以及并行操作對象。該模型反映在圖一中即是,并行同時在主線程中拿取資料進行處理,并線程執行相同的操作,然後計算完成後合并結果。各個并行線程在執行時互不幹擾。

消息傳遞(Message Passing)模型:各個并行執行部分之間傳遞消息,互相通訊。消息傳遞模型的并行線程在執行時會傳遞資料,可能一個線程運作到一半的時候,它所占用的資料或處理結果就要交給另一個線程處理,這樣,在設計并行程式時會給我們帶來一定麻煩。該模型一般是分布式記憶體并行計算機所采用方法,但是也可以适用于共享式記憶體的并行計算機。

什麼時候用并行計算:

多核CPU——計算密集型任務。盡量使用并行計算,可以提高任務執行效率。計算密集型任務會持續地将CPU占滿,此時有越多CPU來分擔任務,計算速度就會越快,這種情況才是并行程式的用武之地。

單核CPU——計算密集型任務。此時的任務已經把CPU資源100%消耗了,就沒必要使用并行計算,畢竟硬體障礙擺在那裡。

單核CPU——I/O密集型任務。I/O密集型任務在任務執行時需要經常調用磁盤、螢幕、鍵盤等外設,由于調用外設時CPU會空閑,是以CPU的使用率并不高,此時使用多線程程式,隻是便于人機互動。計算效率提升不大。

多核CPU——I/O密集型任務。同單核CPU——I/O密集型任務。

GPU即圖形處理器核心(Graphics Processing Unit),它是顯示卡的心髒,顯示卡上還有顯存,GPU與顯存類似與CPU與記憶體。

GPU與CPU有不同的設計目标,CPU需要處理所有的計算指令,是以它的單元設計得相當複雜;而GPU主要為了圖形“渲染”而設計,渲染即進行資料的列處理,是以GPU天生就會為了更快速地執行複雜算術運算和幾何運算的。

GPU相比與CPU有如下優勢:

強大的浮點數計算速度。

大量的計算核心,可以進行大型并行計算。一個普通的GPU也有數千個計算核心。

強大的資料吞吐量,GPU的吞吐量是CPU的數十倍,這意味着GPU有适合的處理大資料。

GPU目前在處理深度學習上用得十分多,英偉達(NVIDIA)目前也花大精力去開發适合深度學習的GPU。現在上百層的神經網絡已經很常見了,面對如此龐大的計算量,CPU可能需要運算幾天,而GPU卻可以在幾小時内算完,這個差距已經足夠别人比我們多打幾個比賽,多發幾篇論文了。

3.3 分布式計算

說到分布式計算,我們就先說下下Google的3篇論文,原文可以直接點連結去下載下傳:

GFS(The Google File System) :解決資料存儲的問題。采用N多台廉價的電腦,使用備援的方式,來取得讀寫速度與資料安全并存的結果。

MapReduce(Simplified Data Processing on Large Clusters) :函數式程式設計,把所有的操作都分成兩類,map與reduce,map用來将資料分成多份,分開處理,reduce将處理後的結果進行歸并,得到最終的結果。

BigTable(Bigtable: A Distributed Storage System for Structured Data) :在分布式系統上存儲結構化資料的一個解決方案,解決了巨大的Table的管理、負載均衡的問題.

Google在2003~2006年發表了這三篇論文之後,一時之間引起了轟動,但是Google并沒有将MapReduce開源。在這種情況下Hadoop就出現了,Doug Cutting在Google的3篇論文的理論基礎上開發了Hadoop,此後Hadoop不斷走向成熟,目前Facebook、IBM、ImageShack等知名公司都在使用Hadoop運作他們的程式。

分布式計算的優勢:

可以內建諸多低配的計算機(成千上萬台)進行高并發的儲存與計算,進而達到與超級計算機媲美的處理能力。

在介紹如何使用python寫并行程式之前,我們需要先補充幾個概念,分别是程序、線程與全局解釋器鎖(Global Interpreter Lock, GIL)。

3.1 程序與線程

程序(process):

在面向線程設計的系統(如當代多數作業系統、Linux 2.6及更新的版本)中,程序本身不是基本運作機關,而是線程的容器。

程序擁有自己獨立的記憶體空間,所屬線程可以通路程序的空間。

程式本身隻是指令、資料及其組織形式的描述,程序才是程式的真正運作執行個體。例如,Visual Studio開發環境就是利用一個程序編輯源檔案,并利用另一個程序完成編譯工作的應用程式。

線程(threading):

線程有自己的一組CPU指令、寄存器與私有資料區,線程的資料可以與同一程序的線程共享。

目前的作業系統是面向線程的,即以線程為基本運作機關,并按線程配置設定CPU。

程序與線程有兩個主要的不同點,其一是程序包含線程,線程使用程序的記憶體空間,當然線程也有自己的私有空間,但容量小;其二是程序有各自獨立的記憶體空間,互不幹擾,而線程是共享記憶體空間。

圖三展示了程序、線程與CPU之間的關系。在圖三中,程序一與程序二都含有3個線程,CPU會按照線程來配置設定任務,如圖中4個CPU同時執行前4個線程,後兩個标紅線程處于等待狀态,在CPU運作完目前線程時,等待的線程會被喚醒并進入CPU執行。通常,程序含有的線程數越多,則它占用CPU的時間會越長。

快速掌握用python寫并行程式,幹貨滿滿

圖三、程序、線程與CPU關系

3.2 全局解釋器鎖GIL:

GIL是計算機程式設計語言解釋器用于同步線程的一種機制,它使得任何時刻僅有一個線程在執行。即便在多核心處理器上,使用 GIL 的解釋器也隻允許同一時間執行一個線程。Python的Cpython解釋器(普遍使用的解釋器)使用GIL,在一個Python解釋器程序内可以執行多線程程式,但每次一個線程執行時就會獲得全局解釋器鎖,使得别的線程隻能等待,由于GIL幾乎釋放的同時就會被原線程馬上獲得,那些等待線程可能剛喚醒,是以經常造成線程不平衡享受CPU資源,此時多線程的效率比單線程還要低下。在python的官方文檔裡,它是這樣解釋GIL的:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

可以說它的初衷是很好的,為了保證線程間的資料安全性;但是随着時代的發展,GIL卻成為了python并行計算的最大障礙,但這個時候GIL已經遍布CPython的各個角落,修改它的工作量太大,特别是對這種開源性的語音來說。但幸好GIL隻鎖了線程,我們可以再建立解釋器程序來實作并行,那這就是multiprocessing的工作了。

3.3 multiprocessing

multiprocessing是python裡的多程序包,通過它,我們可以在python程式裡建立多程序來執行任務,進而進行并行計算。 官方文檔 如下所述:

The multiprocessing package offers both local and remote concurrency, effectively side-stepping the Global Interpreter Lock by using subprocesses instead of threads.

我們接下來介紹下multiprocessing的各個接口:

3.3.1 程序process

示例1

3.3.2 程序池Process Pools

示例2

3.3.3 Pipes & Queues

示例

3.3.4 程序鎖multiprocessing.Lock

3.3.5 共享記憶體——Value, Array

共享記憶體通常需要配合程序鎖來處理,保證處理的順序相同。

快速掌握用python寫并行程式,幹貨滿滿

3.3.6 其它方法

3.3.7 注意事項:

盡量避免共享資料

所有對象都盡量是可以pickle的

避免使用terminate強行終止程序,以造成不可預料的後果

有隊列的程序在終止前隊列中的資料需要清空,join操作應放到queue清空後

明确給子程序傳遞資源、參數

windows平台另需注意:

注意跨子產品全局變量的使用,可能被各個程序修改造成結果不統一

主子產品需要加上if name == ' main ':來提高它的安全性,如果有互動界面,需要加上freeze_support()

process、lock與value嘗試:

上述代碼即對共享記憶體疊加5次,p1程序每次疊加1,p2程序每次疊加3,為了避免p1與p2在運作時搶奪共享資料v,在程序執行時鎖住了該程序,進而保證了執行的順序。我測試了三個案例:

直接運作上述代碼輸出[1, 2, 3, 4, 5, 8, 11, 14, 17, 20],運作時間為1.037s

在1的基礎上注釋掉鎖(上述注釋了三行),在沒有鎖的情況下,輸出[1, 4, 5, 8, 9, 12, 13, 15, 14, 16],運作時間為0.53s

在2的基礎上将p1.join()調到p2.start()前面,輸出為[1, 2, 3, 4, 5, 8, 11, 14, 17, 20],運作時間為1.042s.

可以發現,沒鎖的情況下調整join可以取得與加鎖類似的結果,這是因為join即是阻塞主程序,直至目前程序結束才回到主程序,若将p1.join()放到p1.start()後面,則會馬上阻塞主程序,使得p2要稍後才開始,這與鎖的效果一樣。

如果如上述代碼所示,p1.join()在p2.start()後面,雖然是p1先join(),但這時隻是阻塞了主程序,而p2是兄弟程序,它已經開始了,p1就不能阻止它了,是以這時如果沒鎖的話p1與p2就是并行了,運作時間就是一半,但因為它們争搶共享變量,是以輸出就變得不确定了。

pool

pool其實非常好用,特别是map與apply_async。通過pool這個接口,我們隻有指定可以并行的函數與函數參數清單,它就可以自動幫我們建立多程序池進行并行計算,真的不要太友善。pool特别适用于資料并行模型,假如是消息傳遞模型那還是建議自己通過process來創立程序吧。

總結

小子這次主要是按自己的了解把并行計算理了下,對程序、線程、CPU之間的關系做了下闡述,并把python的multiprocessing這個包拎了拎,個人感覺這個裡面還大有學問,上次我一個師兄用python的process來控制單次疊代的運作時間(運作逾時就跳過這次疊代,進入下一次疊代)也是讓我漲了見識,後面還要多多學習啊。