小豬的Python學習之旅 —— 11.Python併發之threading模塊(2)
標籤:Python
一句話概括本文:
本節繼續把Python裏threading線程模塊剩下的Condition,Semaphore,
Event,Timer和Barrier講解完畢,文檔是枯燥無味的,希望通過簡單
有趣的例子,可以幫你快速掌握這幾個東東的用法~
啃文檔是比較乏味的,先來個小姐姐提提神吧~
別問高清原圖,程序猿自己動手,豐(營)衣(養)足(跟)食(不上),
腳本自取:https://github.com/coder-pig/ReptileSomething
引言:
如果你忘記了threading上一部分內容,可以移步至:
小豬的Python學習之旅 —— 7.Python併發之threading模塊(1)
溫故知新,官方文檔依舊是:
https://docs.python.org/3/library/threading.html
1.條件變量(Condition)
上節學習了Python爲我們提供的第一個用於線程同步的東東——互斥鎖,
又分Lock(指令鎖)與RLock(可重入鎖),但是互斥鎖只是最簡單的同步機制,
Python爲我們提供了Condition(條件變量),以便於處理複雜線程同步問題,
比如最經典的生產者與消費者問題。
Condition除了提供與Lock類似的acquire()與release()函數外,還提供了
wait()與notify()函數。用法如下:
- 1.調用threading.Condition獲得一個條件變量對象;
- 2.線程調用acquire獲得Condition對象;
- 3.進行條件判斷,不滿足條件調用wait函數,滿足條件,進行一些處理改變
條件後,調用notify函數通知處於wait狀態的線程,重新進行條件判斷。
寫個簡單的生產者與消費者例子體驗下:
import threading
import time
condition = threading.Condition()
products = 0 # 商品數量
# 定義生產者線程類
class Producer(threading.Thread):
def run(self):
global products
while True:
if condition.acquire():
if products >= 99:
condition.wait()
else:
products += 2
print(self.name + "生產了2個產品,當前剩餘產品數爲:" + str(products))
condition.notify()
condition.release()
time.sleep(2)
# 定義消費者線程類
class Consumer(threading.Thread):
def run(self):
global products
while True:
if condition.acquire():
if products < 3:
condition.wait()
else:
products -= 3
print(self.name + "消耗了3個產品,當前剩餘產品數爲:" + str(products))
condition.notify()
condition.release()
time.sleep(2)
if __name__ == '__main__':
# 創建五個生產者線程
for i in range(5):
p = Producer()
p.start()
# 創建兩個消費者線程
for j in range(2):
c = Consumer()
c.start()
運行結果:
Condition維護着一個互斥鎖對象(默認是RLock),也可以自己實例化一個
在Condition實例化的時候通過構造函數傳入,SO,調用的Condition的
acquire與release函數,其實調用就是這個鎖對象的acquire與release函數。
下面詳解下除了acquire與release函數外Condition提供的相關函數吧:
(注:下述方法只有在acquire之後才能調用,不然會報RuntimeError異常)
- wait(timeout=None):釋放鎖,同時線程被掛起,直到收到通知被喚醒
或超時(如果設置了timeout),當線程被喚醒並重新佔有鎖時,程序才繼續執行; - wait_for(predicate, timeout=None):等待知道條件爲True,predicate應該是
一個回調函數,返回布爾值,timeout用於指定超時時間,返回值爲回調函數
返回的布爾值,或者超時,返回False(3.2新增); - notify(n=1):默認喚醒一個正在的等待線程,notify並不釋放鎖!!!
- notify_all():喚醒所有等待線程,進入就緒狀態,等待獲得鎖,notify_all 同樣不釋放鎖!!!
2.信號量(Semaphore)
信號量,也是一個很容易懂的東西,舉個簡單的例子:
假如廁所裏有五個蹲坑,有人來開大,就會佔用一個坑位,
所剩餘的坑位-1,當五個坑都被人佔滿的時候,新來的人
就只能在外面等,直到有人出來爲止。
這裏的五個糞坑就是信號量,蹲坑的人就是線程,
初始值爲5,來人-1,走人+1;超過初始值,新來的處於堵塞狀態;
原理很簡單,試試看下源碼:
看下_init_方法
傳入參數value,默認值爲1,不能傳入負數,否則拋ValueError異常;
創建了一個Condition條件變量,傳入一個Lock實例;
接着看下acquire函數:
- 先是判斷,如果沒有加鎖然後設置了超時時間,拋出ValueError;
- 循環,如果value == 0,沒有加鎖或在超時時間內,跳出循環;
否則,調用Condition變量wait函數等待通知或超時; - 如果value不爲0,跳出循環執行else裏的代碼,信號量-1,rc = ture,
代表可以調用release函數,最後返回rc;
再接着是release函數,更簡單
信號量+1,然後調用Condition變量的notify喚醒一個線程~
剩下的_enter_和_exit_就不用說了,重寫這兩個方法就能直接用with關鍵字了
就是那麼簡單,把我們蹲坑的那個例子寫成代碼吧:
import threading
import time
import random
s = threading.Semaphore(5) # 糞坑
class Human(threading.Thread):
def run(self):
s.acquire() # 佔坑
print("拉屎拉屎 - " + self.name + " - " + str(time.ctime()))
time.sleep(random.randrange(1, 3))
print("拉完走人 - " + self.name + " - " + str(time.ctime()))
s.release() # 走人
if __name__ == '__main__':
for i in range(10):
human = Human()
human.start()
輸出結果:
3.通用的條件變量(Event)
Python提供的用於線程間通信的信號標誌,一個線程標識了一個事件,
其他線程處於等待狀態,直到事件發生後,所有線程都會被激活。
Event對象屬性實現了簡單的線程通信機制,提供了設置信號,清楚信號,
等待等用於實現線程間的通信。提供以下四個可供調用的方法:
- is_set():判斷內部標誌是否爲真
- set():設置信號標誌爲真
- clear():清除Event對象內部的信號標誌(設置爲false)
- wait(timeout=None):使線程一直處於堵塞,知道標識符變爲True
感覺有點蒙圈,看一波源碼吧~
先是_init_函數
又是用到Condition條件變量,還有設置了一個_flag = False,這個就是標記吧!
is_set函數比較簡單,返回_flag,
然後是set()函數:
加鎖,然後設置_flag爲true,然後notify_all喚醒所有線程,最後釋放鎖,
簡單,接着clear函數呢?
註釋的意思是:重置內部標記爲false,隨後,調用wait()的線程將被堵塞,
直到調用set()將內部標記再次設置爲true。也很簡單,最後是wait方法:
判斷標誌是否爲False,False的話進入堵塞狀態,(⊙v⊙)嗯
源碼就那麼簡單,感覺看完還是蒙圈不知道怎麼用,寫個簡單的例子?
汽車過紅綠燈的例子:
import threading
import time
import random
class CarThread(threading.Thread):
def __init__(self, event):
threading.Thread.__init__(self)
self.threadEvent = event
def run(self):
# 休眠模擬汽車先後到達路口時間
time.sleep(random.randrange(1, 10))
print("汽車 - " + self.name + " - 到達路口...")
self.threadEvent.wait()
print("汽車 - " + self.name + " - 通過路口...")
if __name__ == '__main__':
light_event = threading.Event()
# 假設有20臺車子
for i in range(20):
car = CarThread(event=light_event)
car.start()
while threading.active_count() > 1:
light_event.clear()
print("紅燈等待...")
time.sleep(3)
print("綠燈通行...")
light_event.set()
time.sleep(2)
輸出結果:
4.定時器(Timer)
與Thread類似,只是要等待一段時間後纔會開始運行,單位秒,用法也很簡單:
import threading
import time
def skill_ready():
print("!!!!!!大招已經準備好了!!!!!!")
if __name__ == '__main__':
t = threading.Timer(5, skill_ready)
t.start()
while threading.active_count() > 1:
print("======大招蓄力中======")
time.sleep(1)
輸出結果:
5.柵欄(Barrier)
Barrier直譯柵欄,感覺不是很好理解,網上有個形象化的例子,把他比喻
成賽馬用的柵欄,然後馬(線程)依次來到柵欄前等待(wait),直到所有的馬
都停在柵欄面前了,然後所有馬開始同時出發(start)。
簡單點說就是,多個線程間的相互等待,調用了wait()方法的線程進入堵塞,
直到所有的線程都調用了wait()方法,然後所有線程同時進行就緒狀態,
等待調度運行。
構造函數:
Barrier(parties,action=None,timeout=None)
- parties:創建一個可容納parties條線程的柵欄;
- action:全部線程被釋放時可被其中一條線程調用的可調用對象;
- timeout:線程調用wait()方法時沒有顯式設定timeout,就用的這個作爲默認值;
相關函數:
- wait(timeout=None):表示線程就位,返回值是一個0到parties-1之間的整數,
每條線程都不一樣,這個值可以用作挑選一條線程做些清掃工作,另外如果你在
構造函數裏設置了action的話,其中一個線程在釋放之前將會調用它。如果調用
出錯的話,會讓柵欄進入broken狀態,超時同樣也會進入broken狀態,如果柵欄
在處於broke狀態的時候調用reset函數,會拋出一個BrokenBarrierError異常。 - reset():本方法將柵欄置爲初始狀態,即empty狀態。所有已經在等待的線程
都會接收到BrokenBarrierError異常,注意當有其他處於unknown狀態的線程時,
調用此方法將可能獲取到額外的訪問。因此如果一個柵欄進入了broken狀態,
最好是放棄他並新建一個柵欄,而不是調用reset方法。 - abort():將柵欄置爲broken狀態。本方法將使所有正在等待或將要調用
wait()方法的線程收到BrokenBarrierError異常。本方法的使用情景爲,比如:
有一條線程需要abort(),又不想給其他線程造成死鎖的狀態,或許設定
timeout參數要比使用本方法更可靠。 - parites:將要使用本 barrier 的線程的數量
- n_waiting:正在等待本 barrier 的線程的數量
- broken:柵欄是否爲broken狀態,返回一個布爾值
BrokenBarrierError:RuntimeError的子類,當柵欄被reset()或broken時引發;
(感覺都不知所云,寫個簡單的例子來熟悉下用法吧~)
例子:公司一起去旅遊
import threading
import time
import random
class Staff(threading.Thread):
def __init__(self, barriers):
threading.Thread.__init__(self)
self.barriers = barriers
def run(self):
print("員工 【" + self.name + "】" + "出門")
time.sleep(random.randrange(1, 10))
print("員工 【" + self.name + "】" + "已簽到")
self.barriers.wait()
def ready():
print(threading.current_thread().name + ":人齊,出發,出發~~~")
if __name__ == '__main__':
print("要出去旅遊啦,大家快集合~")
b = threading.Barrier(10, action=ready, timeout=20)
for i in range(10):
staff = Staff(b)
staff.start()
運行結果:
PS:這裏可以試下設置超時,還有修改ready方法,故意引起異常,
然後會拋出BrokenBarrierError異常。
6.小結
加了下班,終於把threading模塊啃完了,不然爬小姐姐好玩,有成就感,
當然Python線程肯定不止那麼簡單,後面還有隊列這些東西~慢慢來,不急。
下一節開始摳multiprocessing這個進程模塊,又是塊大骨頭,敬請期待~
本節參考文獻:
來啊,Py交易啊
想加羣一起學習Py的可以加下,智障機器人小Pig,驗證信息裏包含:
Python,python,py,Py,加羣,交易,屁眼 中的一個關鍵詞即可通過;
驗證通過後回覆 加羣 即可獲得加羣鏈接(不要把機器人玩壞了!!!)~~~
歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。