小豬的Python學習之旅 —— 11.Python併發之threading模塊(2)

小豬的Python學習之旅 —— 11.Python併發之threading模塊(2)

標籤:Python


一句話概括本文

本節繼續把Python裏threading線程模塊剩下的ConditionSemaphore
EventTimerBarrier講解完畢,文檔是枯燥無味的,希望通過簡單
有趣的例子,可以幫你快速掌握這幾個東東的用法~

啃文檔是比較乏味的,先來個小姐姐提提神吧~

別問高清原圖,程序猿自己動手,豐(營)衣(養)足(跟)食(不上),
腳本自取: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狀態,返回一個布爾值

BrokenBarrierErrorRuntimeError的子類,當柵欄被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,驗證信息裏包含:
PythonpythonpyPy加羣交易屁眼 中的一個關鍵詞即可通過;

驗證通過後回覆 加羣 即可獲得加羣鏈接(不要把機器人玩壞了!!!)~~~
歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。

發佈了306 篇原創文章 · 獲贊 1857 · 訪問量 1661萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章