Python執行緒指南

發表於2015-09-16

1. 執行緒基礎

1.1. 執行緒狀態

執行緒有5種狀態,狀態轉換的過程如下圖所示:

1.2. 執行緒同步(鎖)

多執行緒的優勢在於可以同時執行多個任務(至少感覺起來是這樣)。但是當執行緒需要共享資料時,可能存在資料不同步的問題。考慮這樣一種情況:一個列表裡所有元素都是0,執行緒”set”從後向前把所有元素改成1,而執行緒”print”負責從前往後讀取列表並列印。那麼,可能執行緒”set”開始改的時候,執行緒”print”便來列印列表了,輸出就成了一半0一半1,這就是資料的不同步。為了避免這種情況,引入了鎖的概念。

鎖有兩種狀態——鎖定和未鎖定。每當一個執行緒比如”set”要訪問共享資料時,必須先獲得鎖定;如果已經有別的執行緒比如”print”獲得鎖定了,那麼就讓執行緒”set”暫停,也就是同步阻塞;等到執行緒”print”訪問完畢,釋放鎖以後,再讓執行緒”set”繼續。經過這樣的處理,列印列表時要麼全部輸出0,要麼全部輸出1,不會再出現一半0一半1的尷尬場面。

執行緒與鎖的互動如下圖所示:

1.3. 執行緒通訊(條件變數)

然而還有另外一種尷尬的情況:列表並不是一開始就有的;而是通過執行緒”create”建立的。如果”set”或者”print” 在”create”還沒有執行的時候就訪問列表,將會出現一個異常。使用鎖可以解決這個問題,但是”set”和”print”將需要一個無限迴圈——他們不知道”create”什麼時候會執行,讓”create”在執行後通知”set”和”print”顯然是一個更好的解決方案。於是,引入了條件變數。

條件變數允許執行緒比如”set”和”print”在條件不滿足的時候(列表為None時)等待,等到條件滿足的時候(列表已經建立)發出一個通知,告訴”set” 和”print”條件已經有了,你們該起床幹活了;然後”set”和”print”才繼續執行。

執行緒與條件變數的互動如下圖所示:

1.4. 執行緒執行和阻塞的狀態轉換

最後看看執行緒執行和阻塞狀態的轉換。

阻塞有三種情況:

  • 同步阻塞是指處於競爭鎖定的狀態,執行緒請求鎖定時將進入這個狀態,一旦成功獲得鎖定又恢復到執行狀態;
  • 等待阻塞是指等待其他執行緒通知的狀態,執行緒獲得條件鎖定後,呼叫“等待”將進入這個狀態,一旦其他執行緒發出通知,執行緒將進入同步阻塞狀態,再次競爭條件鎖定;
  • 而其他阻塞是指呼叫time.sleep()、anotherthread.join()或等待IO時的阻塞,這個狀態下執行緒不會釋放已獲得的鎖定。

tips: 如果能理解這些內容,接下來的主題將是非常輕鬆的;並且,這些內容在大部分流行的程式語言裡都是一樣的。(意思就是非看懂不可 >_< 嫌作者水平低找別人的教程也要看懂)

2. thread

Python通過兩個標準庫thread和threading提供對執行緒的支援。thread提供了低階別的、原始的執行緒以及一個簡單的鎖。

thread 模組提供的其他方法:

thread.interrupt_main(): 在其他執行緒中終止主執行緒。
thread.get_ident(): 獲得一個代表當前執行緒的魔法數字,常用於從一個字典中獲得執行緒相關的資料。這個數字本身沒有任何含義,並且當執行緒結束後會被新執行緒複用。

thread還提供了一個ThreadLocal類用於管理執行緒相關的資料,名為 thread._local,threading中引用了這個類。

由於thread提供的執行緒功能不多,無法在主執行緒結束後繼續執行,不提供條件變數等等原因,一般不使用thread模組,這裡就不多介紹了。

3. threading

threading基於Java的執行緒模型設計。鎖(Lock)和條件變數(Condition)在Java中是物件的基本行為(每一個物件都自帶了鎖和條件變數),而在Python中則是獨立的物件。Python Thread提供了Java Thread的行為的子集;沒有優先順序、執行緒組,執行緒也不能被停止、暫停、恢復、中斷。Java Thread中的部分被Python實現了的靜態方法在threading中以模組方法的形式提供。

threading 模組提供的常用方法:

threading.currentThread(): 返回當前的執行緒變數。
threading.enumerate(): 返回一個包含正在執行的執行緒的list。正在執行指執行緒啟動後、結束前,不包括啟動前和終止後的執行緒。
threading.activeCount(): 返回正在執行的執行緒數量,與len(threading.enumerate())有相同的結果。

threading模組提供的類:

Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local.

3.1. Thread

Thread是執行緒類,與Java類似,有兩種使用方法,直接傳入要執行的方法或從Thread繼承並覆蓋run():

構造方法:

Thread(group=None, target=None, name=None, args=(), kwargs={})
group: 執行緒組,目前還沒有實現,庫引用中提示必須是None;
target: 要執行的方法;
name: 執行緒名;
args/kwargs: 要傳入方法的引數。

例項方法:

isAlive(): 返回執行緒是否在執行。正在執行指啟動後、終止前。
get/setName(name): 獲取/設定執行緒名。
is/setDaemon(bool): 獲取/設定是否守護執行緒。初始值從建立該執行緒的執行緒繼承。當沒有非守護執行緒仍在執行時,程式將終止。
start(): 啟動執行緒。
join([timeout]): 阻塞當前上下文環境的執行緒,直到呼叫此方法的執行緒終止或到達指定的timeout(可選引數)。

一個使用join()的例子:

執行結果:

in threadContext.
in threadJoin.
out threadJoin.
out threadContext.

3.2. Lock

Lock(指令鎖)是可用的最低階的同步指令。Lock處於鎖定狀態時,不被特定的執行緒擁有。Lock包含兩種狀態——鎖定和非鎖定,以及兩個基本的方法。

可以認為Lock有一個鎖定池,當執行緒請求鎖定時,將執行緒至於池中,直到獲得鎖定後出池。池中的執行緒處於狀態圖中的同步阻塞狀態。

構造方法:

Lock()

例項方法:

acquire([timeout]): 使執行緒進入同步阻塞狀態,嘗試獲得鎖定。
release(): 釋放鎖。使用前執行緒必須已獲得鎖定,否則將丟擲異常。

3.3. RLock

RLock(可重入鎖)是一個可以被同一個執行緒請求多次的同步指令。RLock使用了“擁有的執行緒”和“遞迴等級”的概念,處於鎖定狀態時,RLock被某個執行緒擁有。擁有RLock的執行緒可以再次呼叫acquire(),釋放鎖時需要呼叫release()相同次數。

可以認為RLock包含一個鎖定池和一個初始值為0的計數器,每次成功呼叫 acquire()/release(),計數器將+1/-1,為0時鎖處於未鎖定狀態。

構造方法:

RLock()

例項方法:

acquire([timeout])/release(): 跟Lock差不多。

3.4. Condition

Condition(條件變數)通常與一個鎖關聯。需要在多個Contidion中共享一個鎖時,可以傳遞一個Lock/RLock例項給構造方法,否則它將自己生成一個RLock例項。

可以認為,除了Lock帶有的鎖定池外,Condition還包含一個等待池,池中的執行緒處於狀態圖中的等待阻塞狀態,直到另一個執行緒呼叫notify()/notifyAll()通知;得到通知後執行緒進入鎖定池等待鎖定。

構造方法:

Condition([lock/rlock])

例項方法:

acquire([timeout])/release(): 呼叫關聯的鎖的相應方法。
wait([timeout]): 呼叫這個方法將使執行緒進入Condition的等待池等待通知,並釋放鎖。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
notify(): 呼叫這個方法將從等待池挑選一個執行緒並通知,收到通知的執行緒將自動呼叫acquire()嘗試獲得鎖定(進入鎖定池);其他執行緒仍然在等待池中。呼叫這個方法不會釋放鎖定。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
notifyAll(): 呼叫這個方法將通知等待池中所有的執行緒,這些執行緒都將進入鎖定池嘗試獲得鎖定。呼叫這個方法不會釋放鎖定。使用前執行緒必須已獲得鎖定,否則將丟擲異常。

例子是很常見的生產者/消費者模式:

3.5. Semaphore/BoundedSemaphore

Semaphore(訊號量)是電腦科學史上最古老的同步指令之一。Semaphore管理一個內建的計數器,每當呼叫acquire()時-1,呼叫release() 時+1。計數器不能小於0;當計數器為0時,acquire()將阻塞執行緒至同步鎖定狀態,直到其他執行緒呼叫release()。

基於這個特點,Semaphore經常用來同步一些有“訪客上限”的物件,比如連線池。

BoundedSemaphore 與Semaphore的唯一區別在於前者將在呼叫release()時檢查計數器的值是否超過了計數器的初始值,如果超過了將丟擲一個異常。

構造方法:

Semaphore(value=1): value是計數器的初始值。

例項方法:

acquire([timeout]): 請求Semaphore。如果計數器為0,將阻塞執行緒至同步阻塞狀態;否則將計數器-1並立即返回。
release(): 釋放Semaphore,將計數器+1,如果使用BoundedSemaphore,還將進行釋放次數檢查。release()方法不檢查執行緒是否已獲得 Semaphore。

3.6. Event

Event(事件)是最簡單的執行緒通訊機制之一:一個執行緒通知事件,其他執行緒等待事件。Event內建了一個初始為False的標誌,當呼叫set()時設為True,呼叫clear()時重置為 False。wait()將阻塞執行緒至等待阻塞狀態。

Event其實就是一個簡化版的 Condition。Event沒有鎖,無法使執行緒進入同步阻塞狀態。

構造方法:

Event()

例項方法:

isSet(): 當內建標誌為True時返回True。
set(): 將標誌設為True,並通知所有處於等待阻塞狀態的執行緒恢復執行狀態。
clear(): 將標誌設為False。
wait([timeout]): 如果標誌為True將立即返回,否則阻塞執行緒至等待阻塞狀態,等待其他執行緒呼叫set()。

3.7. Timer

Timer(定時器)是Thread的派生類,用於在指定時間後呼叫一個方法。

構造方法:

Timer(interval, function, args=[], kwargs={})
interval: 指定的時間
function: 要執行的方法
args/kwargs: 方法的引數

例項方法:

Timer從Thread派生,沒有增加例項方法。

3.8. local

local是一個小寫字母開頭的類,用於管理 thread-local(執行緒區域性的)資料。對於同一個local,執行緒無法訪問其他執行緒設定的屬性;執行緒設定的屬性不會被其他執行緒設定的同名屬性替換。

可以把local看成是一個“執行緒-屬性字典”的字典,local封裝了從自身使用執行緒作為 key檢索對應的屬性字典、再使用屬性名作為key檢索屬性值的細節。

熟練掌握Thread、Lock、Condition就可以應對絕大多數需要使用執行緒的場合,某些情況下local也是非常有用的東西。本文的最後使用這幾個類展示執行緒基礎中提到的場景:

全文完

相關文章