為什么需要條件變量
有了前面提到的互斥鎖扮匠,為什么還需要條件變量呢,當(dāng)然是由于有些復(fù)雜問題互斥鎖搞不定了凡涩。Python
提供的Condition
對(duì)象提供了對(duì)復(fù)雜線程同步問題的支持棒搜。Condition
被稱為條件變量,除了提供與Lock
類似的acquire
和release
方法外活箕,還提供了wait
和notify
方法力麸。
先看一個(gè)互斥鎖解決不了的場(chǎng)景,假設(shè)兩個(gè)智能聊天機(jī)器人(小米的小愛和天貓的天貓精靈)對(duì)話育韩,
天貓精靈:小愛同學(xué)
小愛:在
天貓精靈:我們來對(duì)古詩(shī)吧
小愛:好啊
天貓精靈:我住長(zhǎng)江頭
小愛:不聊了克蚂,再見
假設(shè)小愛和天貓精靈分別是兩個(gè)線程,先使用互斥鎖來實(shí)現(xiàn)一下:
import threading
class XiaoAi(threading.Thread):
def __init__(self, lock):
super().__init__(name="小愛")
self.lock = lock
def run(self):
self.lock.acquire()
print("{} : 在".format(self.name))
self.lock.release()
self.lock.acquire()
print("{} : 好啊".format(self.name))
self.lock.release()
class TianMao(threading.Thread):
def __init__(self, lock):
super().__init__(name="天貓精靈")
self.lock = lock
def run(self):
self.lock.acquire()
print("{} : 小愛同學(xué)".format(self.name))
self.lock.release()
self.lock.acquire()
print("{} : 我們來對(duì)古詩(shī)吧".format(self.name))
self.lock.release()
if __name__ == "__main__":
lock = threading.Lock()
xiaoai = XiaoAi(lock)
tianmao = TianMao(lock)
tianmao.start()
xiaoai.start()
# 運(yùn)行結(jié)果如下:
# 天貓精靈 : 小愛同學(xué)
# 天貓精靈 : 我們來對(duì)古詩(shī)吧
# 小愛 : 在
# 小愛 : 好啊
可以看到筋讨,輸出結(jié)果并不是預(yù)期的對(duì)話順序埃叭,這是因?yàn)樘熵埦`的線程說完“小愛同學(xué)”之后,cpu的控制權(quán)還沒有交出去版仔,繼續(xù)獲取了互斥鎖游盲,又執(zhí)行了“我們來對(duì)古詩(shī)吧”,所以不能得到預(yù)期結(jié)果蛮粮。
先自己想一下解決辦法益缎,理論上應(yīng)該A線程在等待中,B線程在干活然想,干活完畢之后通知A線程活干完了莺奔,B線程進(jìn)入等待,而A線程得到了通知之后变泄,不再繼續(xù)等待令哟,開始干活,看完之后通知B線程妨蛹,如此循環(huán)屏富,直到結(jié)束。
比較粗糙的想法:假設(shè)有一個(gè)全局變量active_user
蛙卤,為0表示該A線程執(zhí)行狠半,1表示B線程執(zhí)行,對(duì)于A線程颤难,先實(shí)現(xiàn)wait
方法:就是while循環(huán)判斷是否active_user == 0
(必須保證這個(gè)變量在兩個(gè)線程中使用的是同一個(gè))神年,notify
方法:將active_user
賦值為1。對(duì)于B線程行嗤,實(shí)現(xiàn)方式相反已日。代碼如下:
import threading
class XiaoAi(threading.Thread):
def __init__(self, lock, active_user):
super().__init__(name="小愛")
self.lock = lock
self.active_user = active_user
def wait(self):
while(1):
self.lock.acquire()
user = self.active_user[0]
self.lock.release()
if user == 1:
break
def notify(self):
self.lock.acquire()
self.active_user[0] = 0
self.lock.release()
def run(self):
self.wait()
print("{} : 在".format(self.name))
self.notify()
self.wait()
print("{} : 好啊".format(self.name))
self.notify()
class TianMao(threading.Thread):
def __init__(self, lock, active_user):
super().__init__(name="天貓精靈")
self.lock = lock
self.active_user = active_user
def wait(self):
while(1):
self.lock.acquire()
user = self.active_user[0]
self.lock.release()
if user == 0:
break
def notify(self):
self.lock.acquire()
self.active_user[0] = 1
self.lock.release()
def run(self):
self.wait()
print("{} : 小愛同學(xué)".format(self.name))
self.notify()
self.wait()
print("{} : 我們來對(duì)古詩(shī)吧".format(self.name))
self.notify()
if __name__ == "__main__":
# 0表示天貓執(zhí)行, 1表示小愛
# 為了保證兩個(gè)線程修改active_user之后,互相是可見的栅屏,所以傳了一個(gè)List,而不是整數(shù)
active_user = [0]
lock = threading.Lock()
xiaoai = XiaoAi(lock, active_user)
tianmao = TianMao(lock, active_user)
tianmao.start()
xiaoai.start()
# 運(yùn)行結(jié)果如下:可得到預(yù)期結(jié)果
# 天貓精靈 : 小愛同學(xué)
# 天貓精靈 : 我們來對(duì)古詩(shī)吧
# 小愛 : 在
# 小愛 : 好啊
由上面的例子可知飘千,由互斥鎖是可以實(shí)現(xiàn)互相通知的需求的堂鲜。但是上面的代碼效率不高,一直在while循環(huán)中判斷占婉,還要自己維護(hù)一個(gè)全局變量泡嘴,很麻煩,在復(fù)雜場(chǎng)景下不能勝任逆济。于是python
就給我們封裝好了Condition
類酌予。
條件變量Condition
構(gòu)造方法:
import threading
# 可傳入一個(gè)互斥鎖或者可重入鎖
cond = threading.Condition()
實(shí)例方法:
acquire([timeout])/release(): 調(diào)用關(guān)聯(lián)的鎖的相應(yīng)方法。
wait([timeout]): 調(diào)用這個(gè)方法將使線程進(jìn)入Condition的等待池等待通知奖慌,并釋放鎖抛虫。
使用前線程必須已獲得鎖定,否則將拋出異常简僧。
notify(): 調(diào)用這個(gè)方法將從等待池挑選一個(gè)線程并通知建椰,收到通知的線程將自動(dòng)調(diào)用
acquire()嘗試獲得鎖定(進(jìn)入鎖定池);其他線程仍然在等待池中岛马。調(diào)用這個(gè)方法不會(huì)
釋放鎖定棉姐。使用前線程必須已獲得鎖定,否則將拋出異常啦逆。
notifyAll(): 調(diào)用這個(gè)方法將通知等待池中所有的線程伞矩,這些線程都將進(jìn)入鎖定池
嘗試獲得鎖定。調(diào)用這個(gè)方法不會(huì)釋放鎖定夏志。使用前線程必須已獲得鎖定乃坤,否則將拋出異常。
主要使用方法和前面自己實(shí)現(xiàn)的差不多沟蔑,主要調(diào)用wait
和notify
方法湿诊,將上面的方法改寫為使用條件變量:
import threading
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name="小愛")
self.cond = cond
def run(self):
self.cond.acquire()
self.cond.wait()
print("{} : 在".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 好啊".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 不聊了,再見".format(self.name))
self.cond.notify()
self.cond.release()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name="天貓精靈")
self.cond = cond
def run(self):
self.cond.acquire()
print("{} : 小愛同學(xué)".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我們來對(duì)古詩(shī)吧".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我住長(zhǎng)江頭".format(self.name))
self.cond.notify()
self.cond.wait()
self.cond.release()
if __name__ == "__main__":
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
tianmao.start()
xiaoai.start()
# 執(zhí)行結(jié)果
# 天貓精靈 : 小愛同學(xué)
運(yùn)行之后會(huì)發(fā)現(xiàn)天貓精靈說出了“小愛同學(xué)”之后就沒有了響應(yīng)瘦材,這就是在使用條件變量的時(shí)候需要注意的點(diǎn)厅须。仔細(xì)觀察主函數(shù)中的線程啟動(dòng)順序,tianmao
先啟動(dòng)了食棕,假設(shè)tianmao
已經(jīng)啟動(dòng)完成九杂,并打印了“小愛同學(xué)”,執(zhí)行notify
之后宣蠕,xiaoai
才剛剛啟動(dòng),成功執(zhí)行完self.cond.acquire()
之后甥捺,開始執(zhí)行wait語(yǔ)句抢蚀,但此時(shí)會(huì)陷入死循環(huán)!原因是 wait()
只能被notify()
喚醒镰禾,而notify()
已經(jīng)被另一個(gè)線程執(zhí)行過了皿曲,注意:只能是一個(gè)線程執(zhí)行過了wait()
唱逢,在被阻塞過程中,另一個(gè)線程執(zhí)行了notify()
才可以屋休。不然就像上面一下陷入死循環(huán)坞古。因此,需要將上面的main
方法改寫:
if __name__ == "__main__":
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
# 啟動(dòng)順序很重要
xiaoai.start()
tianmao.start()
# 執(zhí)行結(jié)果
# 天貓精靈 : 小愛同學(xué)
# 小愛 : 在
# 天貓精靈 : 我們來對(duì)古詩(shī)吧
# 小愛 : 好啊
# 天貓精靈 : 我住長(zhǎng)江頭
# 小愛 : 不聊了劫樟,再見
可以看到痪枫,改完啟動(dòng)順序運(yùn)行結(jié)果對(duì)了,其實(shí)這樣并不能完全保證xiaoai
會(huì)先啟動(dòng)叠艳,如果xiaoai
的run
方法中有個(gè)1s延時(shí)奶陈,就算先執(zhí)行xiaoai.start()
,tianmao
也會(huì)先執(zhí)行notify()
附较,具體這種情況下應(yīng)該怎么辦吃粒,暫時(shí)還不清楚。拒课。徐勃。
源碼分析
大致實(shí)現(xiàn)思路描述:Codition
有兩層鎖,一把底層鎖會(huì)在進(jìn)入wait
方法的時(shí)候釋放早像,離開wait
方法的時(shí)候再次獲取僻肖,上層鎖會(huì)在每次調(diào)用wait
時(shí)分配一個(gè)新的鎖,并放入condition
的等待隊(duì)列中扎酷,而notify
負(fù)責(zé)釋放這個(gè)鎖檐涝。可能理解起來不是很直觀法挨,直接看源碼:
init方法
先看源碼Condition
類的說明谁榜,這是一個(gè)實(shí)現(xiàn)了條件變量的類,允許一個(gè)或多個(gè)線程等待其他線程的通知凡纳。在__init__
方法中窃植,有一個(gè)參數(shù)lock
,默認(rèn)為None
荐糜。有兩種用法:
- 如果
lock
是非None
巷怜,也就是說用戶想自己設(shè)置參數(shù),必須傳遞Lock
或RLock
對(duì)象暴氏。 - 如果
lock
是None
延塑,__init__
方法中默認(rèn)使用可重入鎖RLock
。
這個(gè)lock
作為底層維護(hù)的鎖 underlying lock
答渔,條件變量實(shí)現(xiàn)的關(guān)鍵关带。
__init__
函數(shù)中另一個(gè)比較重要的步驟是,建立了一個(gè)雙端隊(duì)列沼撕,存儲(chǔ)所有在等待中的鎖宋雏,self._waiters = _deque()
wait方法
這里有一個(gè)疑惑芜飘,第二次的waiter.acquire()
沒有找到對(duì)應(yīng)的release
方法?雖然感覺不會(huì)影響結(jié)果磨总,一種可能是在從隊(duì)列中移除這個(gè)鎖的時(shí)候嘗試了釋放這個(gè)鎖嗦明。
notify方法
簡(jiǎn)單總結(jié),A線程阻塞在wait
方法時(shí)蚪燕,只有B線程執(zhí)行了notify
和wait
的時(shí)候(也有可能B線程執(zhí)行了notify
娶牌,而C線程執(zhí)行了wait
),A線程的wait
方法才能執(zhí)行完畢邻薯,而此時(shí)B線程會(huì)阻塞在wait
方法中裙戏。
總結(jié)
- 條件變量提供了對(duì)復(fù)雜線程同步問題的支持。
- 條件變量也是使用互斥鎖實(shí)現(xiàn)的厕诡,主要是兩層鎖結(jié)構(gòu)累榜。