搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

暴躁的熱心網友皮皮文發表於2018-05-31

如果只能用一隻手吃飯+喝湯,吃飯耗時十五分鐘,喝湯五分鐘,這樣肯定耗時。人家還想早點吃完開一把LOL呢,那麼很簡單這時候,我們就會想到左手喝湯,右手吃飯這樣同時進行。這樣時間上吃飯就得到了優化。

多執行緒這個話題自然離不開程式執行緒這兩個keyword

1.程式

首先簡單介紹下程式:計算機程式只是儲存在磁碟上的可執行二進位制(或者其他型別)檔案。從硬碟中讀取他們到記憶體中這樣才擁有了生命週期。程式或者說重量級程式(同義)是一個執行中的程式,它擁有自己的地址空間、記憶體、資料棧以及其他用於跟蹤執行的輔助資料。我們的作業系統(Operating System,簡稱OS)管理所有程式的執行,併為這些程式分配合理的地址空間。程式也可以通過fork或者spawn新的程式來執行其他任務,新的程式當然也擁有自己的記憶體和資料棧,所以只能採用程式間通訊(IPC)的方式。

2.執行緒

執行緒或輕量級程式(同義)與程式類似,不過它們是在統一程式下執行的,並且共享相同的上下文,可以理解一個程式池中的東西執行緒共享。

執行緒包括三部分:開始--執行--結束,指令指標用於記錄當前執行的上下文。當其他執行緒執行時,它可以被搶佔(中斷)和臨時掛起(sleep),這種做法叫做讓步(yielding)。

當然執行緒中也會有主執行緒,所以說一個程式中的各個執行緒與主執行緒共享一片資料空間。如果一頓午飯相當於一個程式的話,你可以吃自己的午飯卻不好意思吃別人的午飯(因為吃別人的午飯需要跟他進行溝通(IPC通訊),溝通不好還會被錘!!!),吃自己的想怎麼吃就怎麼吃。執行緒一般是併發執行的,由於這種併發和資料共享機制,使得多工間協作成為可能。

在單核的CPU中真正的併發是不可能的,在一段時間內你只可能夾一道菜,吃兩口再夾別的菜,之後也可以再回頭吃這個菜。 這樣的一個隨心所欲的夾菜的順序就成了無言的一個排列。執行緒也是一樣,一個執行緒執行一段時間然後開始休眠,讓步給其他的執行緒(再次排隊等待更多的CPU時間)。在整個程式執行過程中,每個執行緒執行它自己特定的任務,在必要的時和其他執行緒進行結果通訊。

這樣難免會聯想出問題,如果共享資料,那麼會有風險。

Q1:

如果兩個或多個執行緒訪問同一片資料,由於資料訪問順序不同,可能導致結果不一致。這種情況就是“競態條件”。不過大多數執行緒庫都有一些同步原語,以允許執行緒管理器控制和訪問。

Q2 :

當然啦 吃飯總會有自己愛吃的菜,所以每道菜的攝入量不會公平。也就是說執行緒無法給予公平的執行時間。如果沒有專門的多執行緒情況進行修改,會導致CPU的時間分配向貪婪函式傾斜。

接下來要談論的就是正題:

python中執行緒

1.GIL(全域性解釋鎖):python程式碼的執行是由Python虛擬機器(直譯器主迴圈)進行控制的。在直譯器主迴圈中只能由一個控制執行緒在執行,就像單核CPU系統的多程式一個道理,記憶體中可以有很多程式,但是在任意給定的時刻只能有一個程式在執行。儘管python直譯器可以執行多個執行緒,但是在任意給定時刻只有一個執行緒會被直譯器執行。

對python虛擬機器的訪問是由全域性解釋鎖GIL控制的,這個鎖就是用來保證同時只能由一個執行緒執行。

在python虛擬機器中將按照下面的方式來執行。

在Python多執行緒下,每個執行緒的執行方式:

1、獲取GIL

2、執行程式碼直到sleep或者是python虛擬機器將其掛起。

**3、釋放GIL **

可見,某個執行緒想要執行,必須先拿到GIL,我們可以把GIL看作是“通行證”,並且在一個python程式中,GIL只有一個。拿不到通行證的執行緒,就不允許進入CPU執行。

在Python2.x裡,GIL的釋放邏輯是當前執行緒遇見IO操作或者ticks計數達到100(ticks可以看作是Python自身的一個計數器,專門做用於GIL,每次釋放後歸零,這個計數可以通過 sys.setcheckinterval 來調整),進行釋放。

而每次釋放GIL鎖,執行緒進行鎖競爭、切換執行緒,會消耗資源。並且由於GIL鎖存在,python裡一個程式永遠只能同時執行一個執行緒(拿到GIL的執行緒才能執行),這就是為什麼在多核CPU上,python的多執行緒效率並不高。

那麼是不是python的多執行緒就完全沒用了呢?
在這裡我們進行分類討論:
1、CPU密集型程式碼(各種迴圈處理、計數等等),在這種情況下,由於計算工作多,ticks計數很快就會達到閾值,然後觸發GIL的釋放與再競爭(多個執行緒來回切換當然是需要消耗資源的),所以python下的多執行緒對CPU密集型程式碼並不友好。

2、IO密集型程式碼(檔案處理、網路爬蟲等),多執行緒能夠有效提升效率(單執行緒下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多執行緒能線上程A等待時,自動切換到執行緒B,可以不浪費CPU的資源,從而能提升程式執行效率)。所以python的多執行緒對IO密集型程式碼比較友好。

2.退出執行緒:

當一個執行緒完成函式執行的時候,它就會退出。還可以通過thread.exit()之類的退出函式,或者sys.exit()之類的退出python程式的標準方法,或者丟擲SystemExit異常,來使執行緒退出。但是你不能直接終止一個執行緒

主執行緒:主執行緒應該是一個好的管理者去管理好排程好好的吃這一頓飯(收集每一個執行緒的結果,生成一個有意義的最終結果。)

3.python中使用執行緒:

一般來說現在我們日常使用的電腦都是64位的所以我們安裝的python直譯器預設都是支援現成的,只需要import好模組即可使用。

thread:這個模組不介紹也不推薦使用,原因是當thread中主執行緒結束時,所有的其他執行緒也都強制結束,不會發出警告或者適當的清理。

threading:常用此模組,因為至少threading模組可以確保重要的子執行緒在程式退出前結束,也成守護執行緒。

看一個簡單的例子

下面的例子是個basic的例子,沒有使用執行緒。吃飯使用5s時間喝湯使用2s時間,是序列的,引數loop是迴圈次數。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

結果:

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

從結果來看喝湯耗時4s,吃飯耗時10s,總共耗時14秒。

加入threading module

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

結果:

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

解析:

建立執行緒陣列,用於執行緒載入,呼叫threading module的Thread()方法建立執行緒,然後呼叫。

結果顯示吃飯喝湯同時進行。start()開始執行緒活動,join()等待執行緒終止。如果不使用join()方法對每個執行緒做等待終止,那麼線上程執行過程中會列印吃完XXX

變強的過程在於思考優化,所以我們可以優化執行緒的建立(執行緒建立程式碼重複冗餘度很高)

那麼我們可以集中遍歷建立,然後基於此目的微調下主函式,結果如下:

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

結果:

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

有點瑕疵應該在print的部分改為i+1,迴圈次數也強制寫死為2次,當然這只是個demo,如果考慮再加個列表讀入times 引數,此時可以新增判斷是否大於零,可以自己試一試不再贅述。

在實際開發中,我們這種利用執行緒的方式肯定不是我們常用的方式,因為自己在用的時候會傳入很多引數,所以我們就必須用到繼承Threading,來建立我們自用的執行緒類

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

結果同之前的就不在展示。

慢慢的你會發現學習一部分慢慢的修改,你的程式碼也會越來越合理,思考的過程就是這樣。

由於目前我們的CPU 都是多核的,所以說每一個程式中都可以有一個執行緒在執行在python中,那假如我們試一試使用多程式,來觀察一下多程式與多執行緒的區別。

多程式模組 multiprocessing模組

多程式模組與threading 模組用法相似,multiprocessing模組可以提供本地和遠端的併發性,有效的通過GIL(全域性解釋所),來使用程式而不是執行緒。在多核CPU中使用多執行緒並不能有效利用CPU的優勢。由於每一個CPU中的程式只能在某一時間執行一個執行緒,所以此時可以考慮多程式來利用多核CPU,UNIX&WIN都是支援的。

將threading修改成multiprocessing即可。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

結果:注意為了區別程式號我在結果中加了列印PID number所以顯示結果如下。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

這樣可以明顯區別開。

PID就是類似於身份證的一個東西,用來表示程式。一個程式在結束前都為一個不變的PID num,但是同一程式可以有不同的PID num,比如多執行兩次此程式你會發現PID num一直在變,或者在自己電腦上執行wechat,反覆開關幾次你也會發現都是不同的PID num。

由上面的程式我們可以發現,multiprocessing與threading的用法沒什麼大差別,也有start()、run()、join()等方法。

看一下文件裡面multiprocessing的用法,如下圖。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

target表示呼叫的物件,args表示呼叫物件的文職引數元組,kwargs表示呼叫物件的字典,name為別名,Group基本用不上,為None也無所謂。

如果去掉PID num,那個從res中完全看不出multiprocessing與threading有什麼區別。*

multiprocessing 之 Queue與pipe:

由於執行緒共享資料,所以不需要通訊,而這點之前也提到過所以,併發程式的情況下,程式就會需要程式通訊(IPC機制)。

常見支援IPC的類 Queue&Pipe傳送常見的物件。 (1)pipe是單向的,也可以是雙向的。通過multiprocessing.Pipe(duplex=False)建立單向管道(預設為雙向,類似於半雙工全雙工的意思,即單為只允許單向傳輸,雙向則允許雙向傳輸。)

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

pipe物件預設是雙向的。在其建立的時候,返回一個含有兩個元素的列表,每個元素代表pipe的一端(Connection物件)。這就成了結果中的對暗號,‘天王蓋地虎,小雞燉蘑菇...不對 寶塔鎮河妖。’

send方法傳送,另一端用recv方法來接收。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

(2)Queue類與Pipe類似,不過學過資料結構,大家都知道佇列都是先進先出。Queue允許多個程式放入,多個程式從佇列存取。

搞懂? Python 多執行緒 多程式(先吃飯再喝湯?還是吃飯喝湯同時進行?)

這裡最需要搞懂的地方就是這個鎖的用處,就好像把程式們出入的時候帶了手銬不讓亂跑,進一個出一個明確,這樣列印起來就不是很亂。

相關文章