1. 程式人生 > >學習筆記-多執行緒

學習筆記-多執行緒

多執行緒

執行緒與程序存在一定區別,每一個程序必須有一個執行緒,執行緒才是程式執行的最小單元程序實際上會在已有的程序空間中執行,在同一個程序裡面,執行緒與執行緒之間是相互獨立,都可以訪問到程序空間裡面的公共變數,而程序與程序之間完全獨立,沒有任何的共享空間,從而導致程序與程序之間的通訊非常麻煩,需要依靠佇列進行完成.而執行緒與執行緒之間則不需要,同在一個程序空間中,存在全域性變數進行相互通訊

使用的是threading庫

import threading
import time


def show1():
    # 執行緒函式輸出
    time.sleep(2)
print('執行緒1') def show2(): # 執行緒函式輸出 print('執行緒2') t1 = threading.Thread(target=show1) t1.start() t2 = threading.Thread(target=show2) t2.start()

執行緒t1和執行緒t2都是建立的,其中target引數是填寫對應的執行函式名,後面跟程序一樣可以進行傳引數,args傳入的必須是元組,kwargs傳入的是字典,說白了就是對應的位置引數和關鍵字引數

多執行緒裡面存在一定缺陷,如果使用的是公共變數進行操作的時候會出現計算混亂或者碰撞的情況,這種情況只有在數值比較大的時候會出現,因為它出現的概率很低

根本原因在於底層的cpu是使用的時間片輪轉的方式進行計算的,當一個程式在執行某個操作全域性變數的語句時,可能執行到一半,還沒有對變數發生改寫,也就是最後的賦值操作,cpu就將其彈出,讓另外的一個程式進行運算,如果此時另外一個程式在cpu內完成了對全域性變數的改寫,此時的全域性變數已經發生改變,cpu再次呼叫原來沒有執行完的程式繼續執行的時候,最後的賦值操作又會對全域性變數進行改寫,這就導致正常運算的程式結果被覆蓋,從而引發計算衝突

import threading

def add1():
    # 進行加法計算
    global num
    for i in range(
1000000): num += 1 print(f'add1完成:{num}') def decrease(): # 進行減法運算 global num for i in range(1000000): num -= 1 print(f'decrease方法完成{num}') num = 0 # 建立執行緒 t1 = threading.Thread(target=add1) t2 = threading.Thread(target=decrease) # 執行執行緒 t1.start() t2.start() print('完成')

最後的執行結果並不是想象中的0,num += 1這種操作實際上分為三步,第一步取出num,第一步計算num+1,第三步將num+1的值賦值給num,如果在第二步的時候就停止了,cpu執行下面的減法,那麼實際上num的值已經出現更改,但是加法還停留在最後的階段沒有賦值,此時它對應的num不是現在已經更改之後的,下一次cpu執行賦值的時候就會導致減法的計算結果被覆蓋,是的這個減法就像沒有被執行過一樣,從而引發計算衝突

互斥鎖

鎖的應用就是解決計算衝突,在程式的執行過程中cpu可能會導致程式執行步驟中斷的情況,這裡python設計了一種鎖的概念,當某個程式一旦加上鎖,在沒有釋放鎖之前該程式將會一執行,直到釋放鎖,這個時候才會遵循cpu的運算時間片輪轉規則.鎖的意義在於鎖住一個最小的執行單元,這個單元不可分割,防止執行到一半被彈出cpu的情況.這就能保證每一次的運算都能夠完成

import threading

def add_function():
    global num
    lock.acquire() # 上鎖
    for i in range(1000000):
        num += 1
    lock.release() # 執行完成解鎖
    print(f'加法完成,結果為{num}')

def decrease():
    global num #申明全域性變數
    lock.acquire() # 上鎖
    for i in range(1000000):
        num -= 1
    lock.release() # 執行完成解鎖
    print(f'減法完成,結果為{num}')



num = 0
lock = threading.Lock()
t1 = threading.Thread(target=add_function)
t2 = threading.Thread(target=decrease)
# 開啟執行緒
t1.start()
t2.start()
print('程式完成')

注意這裡面的鎖沒有上到num+=1上面,按照規範確實是應該加到上面,然而外面就是for迴圈,迴圈次數非常大,每次迴圈都要進行上鎖解鎖,重複非常多次,這會影響整個程式的執行速度,所以將鎖加到for迴圈外面,然而這就導致必須是某個for迴圈結束才能執行另外一個for迴圈,這跟單執行緒幾乎沒有什麼差別,在這個程式碼中確實是這樣,如果兩個函式非常複雜的時候,情況就不同了

死鎖

如果在專案中是多個人進行程式設計執行某個功能的時候,通常都不是單執行緒做的,而是多執行緒做的,每個人在程式設計的過程中都會使用互斥鎖,很多時候出現鎖死的情況,這種情況下系統並不報錯,只是都在等待,如果沒有及時處理,就跟宕機了一樣,這就是死鎖,現在進行死鎖的程式碼演示

import threading
import time


def threading1():
    if lock1.acquire():
        print('鎖上lock1')
        time.sleep(1)
        # 如果想解鎖,那就設定在拿鎖的時候進行實踐設定
        if lock2.acquire(blocking=True,timeout=2): # 這裡就會卡死,因為lock2在threading2中還沒有釋放
            print('鎖上lock2')
        lock1.release()


def threading2():
    if lock2.acquire():
        print('鎖上lock2')
        time.sleep(1)
        # 如果想解鎖,那就設定在拿鎖的時候進行實踐設定
        if lock1.acquire(blocking=True,timeout=2): # 這裡就會卡死,因為lock1在threading1中還沒有釋放
            # 如果兩秒鐘還沒有拿到就跳過
            print('鎖上lock1')
        lock2.release()


if __name__ == '__main__':
    # 建立兩把鎖
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    # 執行兩個執行緒
    t1 = threading.Thread(target=threading1)
    t2 = threading.Thread(target=threading2)
    t1.start()
    t2.start()

如果不進行時間引數設定,則兩個執行緒都會卡死,鎖時間的引數設定意義就在於,當執行緒遇到需要拿的鎖,而這個鎖正好被其他執行緒佔用,那麼當前執行緒則進入等待狀態,如果另外一個執行緒是個死迴圈或者是某個bug導致沒有釋放鎖,則這個執行緒就死了,永遠不會執行,除非鎖被釋放.時間引數就是設定等待對應的時間,如果時間到了還沒有解鎖,則跳過該語句繼續執行後面的程式碼,這就能夠實現防止鎖死的情況

執行緒的同步

執行緒之間是相互獨立執行程式碼,有的時候存線上程同步的需求,比如某個執行緒前期執行一大堆程式碼,當對共同的某個全域性變數做變更的時候需要進行相應的確認,確認這個全域性變數已經滿足需要執行的多執行緒條件的時候進行更改,此時需要等待其他執行緒,這才是執行緒的同步,當然這裡可以使用執行緒.join的方法進行控制,但是這個方法是必須等對應執行緒執行完成,那如果是執行緒中間的某個計算步驟需要確認,後續的程式碼很多,執行時間很長,這裡用執行緒.join()等待顯然不現實

如果建立另外一個判斷用的全域性變數當然是可以進行執行緒之間的公用,但是在判斷語句裡面沒有辦法實現暫停的方式進行等待,如果在判斷的瞬間沒有通過,則程式要麼跳過判斷,要麼終止,因此這種方法也不行.

這裡考慮到需要暫停的特性,使用互斥鎖的方式能夠達到這種效果,執行緒之間是可以通過鎖的方式進行相互通訊,鎖本身可以看出是相互通訊的訊號或者是判斷條件.鎖的特性在於如果處於上鎖狀態,程式就會進行sleep狀態等待,這種狀態不會浪費cpu資源,比使用死迴圈進行監聽要方便的多

import threading
import time


def show1():
    while True:
        if lock1.acquire(): # 如果lock1鎖沒有被鎖則先執行這裡
            print("1")
            time.sleep(1)
            lock2.release() # 執行完成後釋放一個鎖


def show2():
    while True:
        if lock2.acquire(): # show1執行後lock2鎖被釋放,所以這裡lock2.acquire是真
            print("2")
            time.sleep(1)
            lock3.release()


def show3():
    while True:
        if lock3.acquire():
            print("3")
            time.sleep(1)
            lock1.release()


if __name__ == '__main__':
    # 建立三個鎖,先將2和3鎖定
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    lock3 = threading.Lock()
    # 鎖定2和3
    lock2.acquire() # 如果不寫則看到的是錯亂的輸出現象
    lock3.acquire()
    # 建立執行緒
    t1 = threading.Thread(target=show1)
    t2 = threading.Thread(target=show2)
    t3 = threading.Thread(target=show3)
    t1.start()
    t2.start()
    t3.start()

如果都去掉if判斷,不進行鎖限定,所有的while裡面進行死迴圈

while True:
    print('1')

這種情況則會在輸出的結果中看到123,321,312等等錯亂的順序結果,這就是因為執行緒執行相互獨立,我們這裡沒有辦法規定哪個函式先執行.

加上鎖之後就能夠實現以我們想要的順序執行,當一個鎖開啟後必然關閉另外一個鎖,保持三個鎖中隨時只有一個鎖開啟,這種方式就能夠實現執行緒同步

當然有人會絕對這沒有什麼卵用,如果要進行順序規定輸出,使用單執行緒的面向過程程式設計思想就可以了,而這裡使用多執行緒多此一舉,我必須承認在這裡是這樣的,但是需要注意的是,這裡每一個函式都很簡單,如果每一個函式都是複雜的功能,在判斷鎖之前各自執行非常複雜的運算,那麼此時使用單執行緒則會導致cpu和記憶體資源的浪費,本來三執行緒跑完用1個小時,單執行緒則需要3個小時