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

coder-pig發表於2018-02-07

小豬的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。

相關文章