[python] 線程間同步之條件變量Condition

為什么需要條件變量

有了前面提到的互斥鎖扮匠,為什么還需要條件變量呢,當(dāng)然是由于有些復(fù)雜問題互斥鎖搞不定了凡涩。Python提供的Condition對(duì)象提供了對(duì)復(fù)雜線程同步問題的支持棒搜。Condition被稱為條件變量,除了提供與Lock類似的acquirerelease方法外活箕,還提供了waitnotify方法力麸。

先看一個(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)用waitnotify方法湿诊,將上面的方法改寫為使用條件變量:

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)叠艳,如果xiaoairun方法中有個(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方法

init方法.png

先看源碼Condition類的說明谁榜,這是一個(gè)實(shí)現(xiàn)了條件變量的類,允許一個(gè)或多個(gè)線程等待其他線程的通知凡纳。在__init__方法中窃植,有一個(gè)參數(shù)lock,默認(rèn)為None荐糜。有兩種用法:

  1. 如果lock是非None巷怜,也就是說用戶想自己設(shè)置參數(shù),必須傳遞LockRLock對(duì)象暴氏。
  2. 如果lockNone延塑,__init__方法中默認(rèn)使用可重入鎖RLock

這個(gè)lock作為底層維護(hù)的鎖 underlying lock答渔,條件變量實(shí)現(xiàn)的關(guān)鍵关带。

__init__函數(shù)中另一個(gè)比較重要的步驟是,建立了一個(gè)雙端隊(duì)列沼撕,存儲(chǔ)所有在等待中的鎖宋雏,self._waiters = _deque()

wait方法

wait方法.png

這里有一個(gè)疑惑芜飘,第二次的waiter.acquire()沒有找到對(duì)應(yīng)的release方法?雖然感覺不會(huì)影響結(jié)果磨总,一種可能是在從隊(duì)列中移除這個(gè)鎖的時(shí)候嘗試了釋放這個(gè)鎖嗦明。

notify方法

notify方法.png

簡(jiǎn)單總結(jié),A線程阻塞在wait方法時(shí)蚪燕,只有B線程執(zhí)行了notifywait的時(shí)候(也有可能B線程執(zhí)行了notify娶牌,而C線程執(zhí)行了wait),A線程的wait方法才能執(zhí)行完畢邻薯,而此時(shí)B線程會(huì)阻塞在wait方法中裙戏。

總結(jié)

  • 條件變量提供了對(duì)復(fù)雜線程同步問題的支持。
  • 條件變量也是使用互斥鎖實(shí)現(xiàn)的厕诡,主要是兩層鎖結(jié)構(gòu)累榜。

參考

  1. python 線程之 Condition
  2. Python3高級(jí)編程和異步IO并發(fā)編程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市灵嫌,隨后出現(xiàn)的幾起案子壹罚,更是在濱河造成了極大的恐慌,老刑警劉巖寿羞,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猖凛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡绪穆,警方通過查閱死者的電腦和手機(jī)辨泳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玖院,“玉大人菠红,你說我怎么就攤上這事∧丫” “怎么了试溯?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)郊酒。 經(jīng)常有香客問我遇绞,道長(zhǎng),這世上最難降的妖魔是什么燎窘? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任摹闽,我火速辦了婚禮,結(jié)果婚禮上褐健,老公的妹妹穿的比我還像新娘钩骇。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布倘屹。 她就那樣靜靜地躺著,像睡著了一般慢叨。 火紅的嫁衣襯著肌膚如雪纽匙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天拍谐,我揣著相機(jī)與錄音烛缔,去河邊找鬼。 笑死轩拨,一個(gè)胖子當(dāng)著我的面吹牛践瓷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亡蓉,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼晕翠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了砍濒?” 一聲冷哼從身側(cè)響起淋肾,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤条获,失蹤者是張志新(化名)和其女友劉穎可霎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體短蜕,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杠河,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年碌尔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片券敌。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡唾戚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陪白,到底是詐尸還是另有隱情颈走,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布咱士,位于F島的核電站立由,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏序厉。R本人自食惡果不足惜锐膜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弛房。 院中可真熱鬧道盏,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至种远,卻和暖如春涩澡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背坠敷。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工妙同, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人膝迎。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓粥帚,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親限次。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芒涡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • 接著上節(jié) atomic,本節(jié)主要介紹condition_varible的內(nèi)容掂恕,練習(xí)代碼地址拖陆。本文參考http://...
    jorion閱讀 8,500評(píng)論 0 7
  • 線程安全 當(dāng)多個(gè)線程訪問一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行懊亡,也不需要進(jìn)行額外的同步依啰,或...
    閩越布衣閱讀 769評(píng)論 0 6
  • Java-Review-Note——4.多線程 標(biāo)簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,655評(píng)論 2 17
  • 【幸福女孩 糖糖 一年級(jí) 堅(jiān)持原創(chuàng)分享第163天 2018.2.12 星期一】 今天我在家做了一個(gè)玩偶店枣,用...
    何亞珂閱讀 216評(píng)論 0 0
  • 在同一個(gè)班里速警,熱熱鬧鬧的,上課老師不管我們鸯两。卻有一天闷旧,老師和我換了一個(gè)同桌,在那個(gè)時(shí)候我和他完全不熟钧唐。但是忙灼,時(shí)間一...
    萌殺閱讀 291評(píng)論 0 0