一篇文章理清Python多線程同步鎖,死鎖和遞歸鎖

前面說到過python多線程的基本使用,大概的內(nèi)容有幾點(diǎn)

1.創(chuàng)建線程對象
t1 = threading.Thread(target=say,args=('tony',))
2.啟動線程
t1.start()
后面又說了兩個點(diǎn)就是join和守護(hù)線程的概念

但是不知道大家有沒有注意到一點(diǎn)就是前面說的兩個功能是相互獨(dú)立的,相互不干涉的,不會用到同享的資源或者數(shù)據(jù),如果我們多個線程要用到相同的數(shù)據(jù),那么就會存在資源爭用和鎖的問題,不管在什么語言中,這個都是不能避免的。對數(shù)據(jù)庫屬性的同學(xué)應(yīng)該也了解,數(shù)據(jù)庫中也存在鎖的概念。

今天這篇文章我們說說python多線程中的同步鎖,死鎖和遞歸鎖的使用。

  1. Python同步鎖

鎖通常被用來實(shí)現(xiàn)對共享資源的同步訪問。為每一個共享資源創(chuàng)建一個Lock對象,當(dāng)你需要訪問該資源時,調(diào)用acquire方法來獲取鎖對象(如果其它線程已經(jīng)獲得了該鎖,則當(dāng)前線程需等待其被釋放),待資源訪問完后,再調(diào)用release方法釋放鎖。

下面我們來舉個例子說明如果多線程在沒有同步鎖的情況下訪問公共資源會導(dǎo)致什么情況

import threading
import time

num = 100

def fun_sub():
    global num
    # num -= 1
    num2 = num
    time.sleep(0.001)
    num = num2-1

if __name__ == '__main__':
    print('開始測試同步鎖 at %s' % time.ctime())

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('結(jié)束測試同步鎖 at %s' % time.ctime())

上面的例子其實(shí)很簡單就是創(chuàng)建100的線程,然后每個線程去從公共資源num變量去執(zhí)行減1操作,按照正常情況下面,等到代碼執(zhí)行結(jié)束,打印num變量,應(yīng)該得到的是0,因?yàn)?00個線程都去執(zhí)行了一次減1的操作。

但是結(jié)果卻不是我們想想的,我們看看結(jié)果

開始測試同步鎖 at Sun Apr 28 09:56:45 2019
num is 91
結(jié)束測試同步鎖 at Sun Apr 28 09:56:45 2019

我們會發(fā)現(xiàn),每次執(zhí)行的結(jié)果num值都不是一樣的,上面顯示的是91,那就存在問題了,為什么結(jié)果不是0呢?

我們來看看上面代碼的執(zhí)行流程。
1.因?yàn)镚IL,只有一個線程(假設(shè)線程1)拿到了num這個資源,然后把變量賦值給num2,sleep 0.001秒,這時候num=100
2.當(dāng)?shù)谝粋€線程sleep 0.001秒這個期間,這個線程會做yield操作,就是把cpu切換給別的線程執(zhí)行(假設(shè)線程2拿到個GIL,獲得cpu使用權(quán)),線程2也和線程1一樣也拿到num,返回賦值給num2,然sleep,這時候,其實(shí)num還是=100.
3.線程2 sleep時候,又要yield操作,假設(shè)線程3拿到num,執(zhí)行上面的操作,其實(shí)num有可能還是100
4.等到后面cpu重新切換給線程1,線程2,線程3上執(zhí)行的時候,他們執(zhí)行減1操作后,其實(shí)等到的num其實(shí)都是99,而不是順序遞減的。
5.其他剩余的線程操作如上

大家應(yīng)該發(fā)現(xiàn)問題了,結(jié)果和我們想想的不一樣,那我們怎么才能等到我們想要的結(jié)果呢?就是100個線程操作num變量得到最后結(jié)果為0?

這里就要借助于python的同步鎖了,也就是同一時間只能放一個線程來操作num變量,減1之后,后面的線程操作來操作num變量。看看下面我們怎么實(shí)現(xiàn)。

import threading
import time

num = 100

def fun_sub():
    global num
    lock.acquire()
    print('----加鎖----')
    print('現(xiàn)在操作共享資源的線程名字是:',t.name)
    num2 = num
    time.sleep(0.001)
    num = num2-1
    lock.release()
    print('----釋放鎖----')

if __name__ == '__main__':
    print('開始測試同步鎖 at %s' % time.ctime())

    lock = threading.Lock() #創(chuàng)建一把同步鎖

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('結(jié)束測試同步鎖 at %s' % time.ctime())

看到上面我們給中間的減1代碼塊,加個一把同步鎖,這樣,我們就可以得到我們想要的結(jié)果了,這就是同步鎖的作用,一次只有一個線程操作同享資源。

看看上面代碼執(zhí)行的結(jié)果:

.......
----加鎖----
現(xiàn)在操作共享資源的線程名字是: Thread-98
----釋放鎖----
----加鎖----
現(xiàn)在操作共享資源的線程名字是: Thread-100
----釋放鎖----
num is 0
結(jié)束測試同步鎖 at Sun Apr 28 12:08:27 2019
  1. Python死鎖

死鎖的這個概念在很多地方都存在,比較在數(shù)據(jù)中,大概介紹下私有是怎么產(chǎn)生的
1.A拿了一個蘋果
2.B拿了一個香蕉
3.A現(xiàn)在想再拿個香蕉,就在等待B釋放這個香蕉
4.B同時想要再拿個蘋果,這時候就等待A釋放蘋果
5.這樣就是陷入了僵局,這就是生活中的死鎖

python中在線程間共享多個資源的時候,如果兩個線程分別占有一部分資源并且同時等待對方的資源,就會造成死鎖,因?yàn)橄到y(tǒng)判斷這部分資源都正在使用,所有這兩個線程在無外力作用下將一直等待下去。下面是一個死鎖的例子:

import threading
import time

lock_apple = threading.Lock()
lock_banana = threading.Lock()

class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

        lock_apple.acquire()  # 如果鎖被占用,則阻塞在這里,等待鎖的釋放

        print ("線程 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime()))

        lock_banana.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        lock_banana.release()
        lock_apple.release()


    def fun2(self):

        lock_banana.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        time.sleep(0.1)

        lock_apple.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime()))
        lock_apple.release()

        lock_banana.release()

if __name__ == "__main__":
    for i in range(0, 10):  #建立10個線程
        my_thread = MyThread()  #類繼承法是python多線程的另外一種實(shí)現(xiàn)方式
        my_thread.start()
代碼執(zhí)行hung住,死鎖了

線程 Thread-1 , 想拿: 蘋果--Sun Apr 28 12:21:06 2019
線程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
線程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
線程 Thread-2 , 想拿: 蘋果--Sun Apr 28 12:21:06 2019

Process finished with exit code -1

上面的代碼其實(shí)就是描述了蘋果和香蕉的故事。大家可以仔細(xì)看看過程。下面我們看看執(zhí)行流程

1.fun1中,線程1先拿了蘋果,然后拿了香蕉,然后釋放香蕉和蘋果,然后再在fun2中又拿了香蕉,sleep 0.1秒。
2.在線程1的執(zhí)行過程中,線程2進(jìn)入了,因?yàn)樘O果被線程1釋放了,線程2這時候獲得了蘋果,然后想拿香蕉
3.這時候就出現(xiàn)問題了,線程一拿完香蕉之后想拿蘋果,返現(xiàn)蘋果被線程2拿到了,線程2拿到蘋果執(zhí)行,想拿香蕉,發(fā)現(xiàn)香蕉被線程1持有了
4.雙向等待,出現(xiàn)死鎖,代碼執(zhí)行不下去了

上面就是大概的執(zhí)行流程和死鎖出現(xiàn)的原因。在這種情況下就是在同一線程中多次請求同一資源時候出現(xiàn)的問題。

  1. Python遞歸鎖RLock

為了支持在同一線程中多次請求同一資源,python提供了"遞歸鎖":threading.RLock。RLock內(nèi)部維護(hù)著一個Lock和一個counter變量,counter記錄了acquire的次數(shù),從而使得資源可以被多次acquire。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。

下面我們用遞歸鎖RLock解決上面的死鎖問題

import threading
import time

lock = threading.RLock()  #遞歸鎖


class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

        lock.acquire()  # 如果鎖被占用,則阻塞在這里,等待鎖的釋放

        print ("線程 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime()))

        lock.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        lock.release()
        lock.release()


    def fun2(self):

        lock.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        time.sleep(0.1)

        lock.acquire()
        print ("線程 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime()))
        lock.release()

        lock.release()

if __name__ == "__main__":
    for i in range(0, 10):  #建立10個線程
        my_thread = MyThread()  #類繼承法是python多線程的另外一種實(shí)現(xiàn)方式
        my_thread.start()

上面我們用一把遞歸鎖,就解決了多個同步鎖導(dǎo)致的死鎖問題。大家可以把RLock理解為大鎖中還有小鎖,只有等到內(nèi)部所有的小鎖,都沒有了,其他的線程才能進(jìn)入這個公共資源。

另外一點(diǎn)前面沒有就算用類繼承的方法實(shí)現(xiàn)python多線程,這個大家可以查下,就算繼承Thread類,然后重新run方法來實(shí)現(xiàn)。

最后大家可能還有個疑問,就算如果我們都加鎖了,也就是單線程了,那我們還要開多線程有什么用呢?這里解釋下,在訪問共享資源的時候,鎖是一定要存在了,但是我們的代碼中不是總是在訪問公共資源的,還有一些其他的邏輯可以使用多線程,所以我們在代碼里面加鎖的時候,要注意在什么地方加,對性能的影響最小,這個就靠對邏輯的理解了。

好了今天就說到這里,個人意見,望指教。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應(yīng)用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進(jìn)行...
    月亮是我踢彎得閱讀 6,173評論 3 28
  • 線程的定義 線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程中。是進(jìn)程中的實(shí)際運(yùn)作單位。一條線程指的是進(jìn)程...
    So_ProbuING閱讀 382評論 0 0
  • 【threading模塊詳解】 模塊基本方法 該模塊定了的方法如下:threading.active_count(...
    奕劍聽雨閱讀 1,154評論 0 0
  • 線程 操作系統(tǒng)線程理論 線程概念的引入背景 進(jìn)程 之前我們已經(jīng)了解了操作系統(tǒng)中進(jìn)程的概念,程序并不能單獨(dú)運(yùn)行,只有...
    go以恒閱讀 1,798評論 0 6
  • 關(guān)于Python多線程的概述 由于GIL的存在,Python的多線程在CPU密集型任務(wù)并沒有多大的優(yōu)勢,任何Pyt...
    千鳥月讀閱讀 597評論 0 0

友情鏈接更多精彩內(nèi)容