python多線程共享全局變量及互斥鎖問題解析

 喜歡編程,熱愛分享,希望能結交更多志同道合的朋友,一起在學習Python的道路上走得更遠!有不懂的問題可以私聊我哦!

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread
num = 0
def addnum():
    global num
    for i in range(1000):
        num += 1
    print(num)
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('主進程結束,num的值爲:',num)

結果爲:

10002000
主進程結束,num的值爲: 2000

可以看到,兩個線程間是共享了num的值。雖說多線程是共享同一塊內存空間的,但是由於每個線程的執行時間是不確定的,都是由CPU來分配的,這就造成 在處理全局變量的時候,有可能線程t1和t2都同時對全局變量num進程操作,比如num原來是10,同時進程加1之 後,由於他們獲取到的num值都是10,因此同時進行加1的時候就只能讓num的值變爲11,假如是當有一個線程在 對num進程操作時,另一個線程等待之前的線程操作完成再去操作的話就不會出現上面的情況。

下面的代碼演示了CPU時間片切換的明顯現象:

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread
import time
list1 = []
def addnum(seq):
    global list1
    for i in seq:
        time.sleep(0.1) # 通過時間等待,可以清楚的看到時間片切換的痕跡
        list1.append(i)
if __name__ == '__main__':
    nums = range(10)
    string = 'abcdefg'
    t1 = Thread(target=addnum, args=(nums,))
    t2 = Thread(target=addnum, args=(string,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(list1)

結果爲:

[0, 'a', 'b', 1, 2, 'c', 3, 'd', 'e', 4, 5, 'f', 6, 'g', 7, 8, 9]

我是一名python開發工程師,整理了一套python的學習資料,從基礎的python腳本到web開發、爬蟲、
數據分析、數據可視化、機器學習、面試真題等。想要的可以進羣:688244617免費領取!

可以看到,線程之間是由時間片來切換的。


2. 線程同步
線程同步說的就是上面多線程對全局變量進程操作的問題,因爲後一個操作要依靠前一個操作的結果,因此 後面的操作必須等待前面的操作完成才能繼續操作,否則就會造成數據缺失。


3. 互斥鎖
互斥鎖是python多線程爲了解決多線程對相同資源競爭而提供的一種鎖機制,互斥鎖有兩種狀態:上鎖/釋放 鎖。這兩種狀態是互斥的,也就是同一把鎖一旦上鎖就必須等待釋放鎖之後才能再次上鎖。多線程中,同一 把鎖一旦有一個線程上鎖,其他的線程必須等待該鎖釋放之後才能繼續上鎖。就像排隊買票一樣,同一個窗 口,後面的人必須等待前面的人買完了才能上去買。

python多線程可以通過Lock類來實現互斥鎖機制。

#!/usr/bin/env python3# -*- coding: utf-8 -*-
from threading import Thread,Lock
num = 0
mutex = Lock() #互斥鎖實例對象
def addnum():
    global num
    for i in range(1000):
        mutex.acquire() # 上鎖
        num += 1
        mutex.release() # 解鎖
    print(num)
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('主進程結束,num的值爲:',num)

結果爲:

1000
2000
主進程結束,num的值爲: 2000

Lock 類實例有兩個主要的方法,acquire以及release方法。

acquire()方法就是讓線程獲得鎖,也就是上鎖,從上鎖到釋放鎖中間的代碼都是線程安全的,只有獲得鎖的 線程才能執行,直到釋放鎖,其他的線程才能在獲得鎖之後再次對代碼進行操作。
release()釋放鎖。解除當前線程對該鎖的佔用。

4. 線程死鎖

線程試圖對同一個互斥量A加鎖兩次

線程1擁有A鎖,請求獲得B鎖;線程2擁有B鎖,請求獲得A鎖
也就是說,兩個線程分別各自獲得一把鎖,然後在沒有釋放自己擁有的鎖的同時想要獲得對方的鎖。就像兩 個人吵架,誰都不想先認錯,互相僵持住了。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread, Lock
import time
mutex1 = Lock() # 實例化鎖A
mutex2 = Lock() # 實例化鎖A 
def workA():
    mutex1.acquire()
    print("線程A獲得鎖A")
    time.sleep(1) # 延時操作,讓線程B先獲得鎖B   
    mutex2.acquire()
    print("線程A獲得鎖B")
    mutex2.release()
    print("線程A釋放鎖B")
    mutex1.release()
    print("線程A釋放鎖A")
def workB():
    mutex2.acquire()
    print("線程B獲得鎖B")
    time.sleep(1) # 延時操作,讓線程A先獲得鎖A    
    mutex1.acquire()
    print("線程B獲得鎖A")
    mutex1.release()
    print("線程B釋放鎖A")
    mutex2.release()
    print("線程B釋放鎖B")
if __name__ == "__main__":
    t1 = Thread(target=workA)
    t2 = Thread(target=workB)
    t1.start()
    t2.start()

結果爲:

線程A獲得鎖A
線程B獲得鎖B

結果是程序就一直卡在接下來的過程那裏,沒辦法執行下去了。

爲了避免死鎖,請不要讓同一個線程,在同一個竟態資源內請求多把鎖。一般我們的爬蟲也只是用到一把鎖而已。 用不到兩把鎖的。而且大部分時間是使用消息隊列來實現共享資源處理的問題。

5. 守護線程


由於我們的線程是在同一個進程中的,因此當我們的主線程(主進程)退出的時候,那麼所有的線程也會被 強行退出。默認情況下用threading模塊創建出來的線程,如果子線程沒有結束,但是主線程執行完了,那麼 主線程會等待所有子線程執行完成之後纔會退出程序(這種情況就是非守護的)。如果想要所有子線程在主 線程執行完成之後就馬上退出,那麼這個時候就需要將所有子線程設置爲守護線程(也就是說明這個線程“不 重要”可以隨着主線程一起退出)。即在調用start()方法之前調用setDaemon()方法將線程的daemon標誌設爲 True(setDaemon(True))。

非守護模式:(主線程執行完畢後,等待子線程結束後才一起退出程序)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread,Lock
import time
num = 0
mutex = Lock() #互斥鎖實例對象
def addnum():
    global num
    for i in range(10):
        time.sleep(1)
        mutex.acquire() # 上鎖
        num += 1
        mutex.release() # 解鎖
    print('子進程結束')
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.start()
    t2.start()
    print('主進程結束,num的值爲:',num)

結果爲:

主進程結束,num的值爲: 0
子進程結束
子進程結束

守護模式(主線程結束後,不管子線程是否結束都一起退出程序):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from threading import Thread,Lock
import time
num = 0
mutex = Lock() #互斥鎖實例對象
def addnum():
    global num
    for i in range(10):
        time.sleep(1)
        mutex.acquire() # 上鎖
        num += 1
        mutex.release() # 解鎖
    print('子進程結束')
if __name__ == '__main__':
    t1 = Thread(target=addnum)
    t2 = Thread(target=addnum)
    t1.setDaemon(True)
    t2.setDaemon(True)
    t1.start()
    t2.start()
    print('主進程結束,num的值爲:',num)

結果爲:

主進程結束,num的值爲: 0

守護線程在多線程爬蟲中會經常使用。覺得寫的不錯的話不妨點個贊,有任何建議或看法歡迎大家在評論區分享討論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章