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

前面說到過python多線程的基本使用,大概的內容有幾點python

1.建立線程對象
t1 = threading.Thread(target=say,args=('tony',))
2.啓動線程
t1.start()
後面又說了兩個點就是join和守護線程的概念
複製代碼

可是不知道你們有沒有注意到一點就是前面說的兩個功能是相互獨立的,相互不干涉的,不會用到同享的資源或者數據,若是咱們多個線程要用到相同的數據,那麼就會存在資源爭用和鎖的問題,無論在什麼語言中,這個都是不能避免的。對數據庫屬性的同窗應該也瞭解,數據庫中也存在鎖的概念。數據庫

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

  1. Python同步鎖

鎖一般被用來實現對共享資源的同步訪問。爲每個共享資源建立一個Lock對象,當你須要訪問該資源時,調用acquire方法來獲取鎖對象(若是其它線程已經得到了該鎖,則當前線程需等待其被釋放),待資源訪問完後,再調用release方法釋放鎖。多線程

下面咱們來舉個例子說明若是多線程在沒有同步鎖的狀況下訪問公共資源會致使什麼狀況app

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('結束測試同步鎖 at %s' % time.ctime())
複製代碼

上面的例子其實很簡單就是建立100的線程,而後每一個線程去從公共資源num變量去執行減1操做,按照正常狀況下面,等到代碼執行結束,打印num變量,應該獲得的是0,由於100個線程都去執行了一次減1的操做。性能

可是結果卻不是咱們想一想的,咱們看看結果測試

開始測試同步鎖 at Sun Apr 28 09:56:45 2019
num is 91
結束測試同步鎖 at Sun Apr 28 09:56:45 2019
複製代碼

咱們會發現,每次執行的結果num值都不是同樣的,上面顯示的是91,那就存在問題了,爲何結果不是0呢?ui

咱們來看看上面代碼的執行流程。
1.由於GIL,只有一個線程(假設線程1)拿到了num這個資源,而後把變量賦值給num2,sleep 0.001秒,這時候num=100
2.當第一個線程sleep 0.001秒這個期間,這個線程會作yield操做,就是把cpu切換給別的線程執行(假設線程2拿到個GIL,得到cpu使用權),線程2也和線程1同樣也拿到num,返回賦值給num2,然sleep,這時候,其實num仍是=100.
3.線程2 sleep時候,又要yield操做,假設線程3拿到num,執行上面的操做,其實num有可能仍是100
4.等到後面cpu從新切換給線程1,線程2,線程3上執行的時候,他們執行減1操做後,其實等到的num其實都是99,而不是順序遞減的。
5.其餘剩餘的線程操做如上
複製代碼

你們應該發現問題了,結果和咱們想一想的不同,那咱們怎麼才能等到咱們想要的結果呢?就是100個線程操做num變量獲得最後結果爲0?spa

這裏就要藉助於python的同步鎖了,也就是同一時間只能放一個線程來操做num變量,減1以後,後面的線程操做來操做num變量。看看下面咱們怎麼實現。線程

import threading
import time

num = 100

def fun_sub():
    global num
    lock.acquire()
    print('----加鎖----')
    print('如今操做共享資源的線程名字是:',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() #建立一把同步鎖

    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('結束測試同步鎖 at %s' % time.ctime())
複製代碼

看到上面咱們給中間的減1代碼塊,加個一把同步鎖,這樣,咱們就能夠獲得咱們想要的結果了,這就是同步鎖的做用,一次只有一個線程操做同享資源。

看看上面代碼執行的結果:

.......
----加鎖----
如今操做共享資源的線程名字是: Thread-98
----釋放鎖----
----加鎖----
如今操做共享資源的線程名字是: Thread-100
----釋放鎖----
num is 0
結束測試同步鎖 at Sun Apr 28 12:08:27 2019
複製代碼
  1. Python死鎖

死鎖的這個概念在不少地方都存在,比較在數據中,大概介紹下私有是怎麼產生的 1.A拿了一個蘋果 2.B拿了一個香蕉 3.A如今想再拿個香蕉,就在等待B釋放這個香蕉 4.B同時想要再拿個蘋果,這時候就等待A釋放蘋果 5.這樣就是陷入了僵局,這就是生活中的死鎖

python中在線程間共享多個資源的時候,若是兩個線程分別佔有一部分資源而且同時等待對方的資源,就會形成死鎖,由於系統判斷這部分資源都正在使用,全部這兩個線程在無外力做用下將一直等待下去。下面是一個死鎖的例子:

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多線程的另一種實現方式
        my_thread.start()
代碼執行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
複製代碼

上面的代碼其實就是描述了蘋果和香蕉的故事。你們能夠仔細看看過程。下面咱們看看執行流程

1.fun1中,線程1先拿了蘋果,而後拿了香蕉,而後釋放香蕉和蘋果,而後再在fun2中又拿了香蕉,sleep 0.1秒。 2.在線程1的執行過程當中,線程2進入了,由於蘋果被線程1釋放了,線程2這時候得到了蘋果,而後想拿香蕉 3.這時候就出現問題了,線程一拿完香蕉以後想拿蘋果,返現蘋果被線程2拿到了,線程2拿到蘋果執行,想拿香蕉,發現香蕉被線程1持有了 4.雙向等待,出現死鎖,代碼執行不下去了

上面就是大概的執行流程和死鎖出現的緣由。在這種狀況下就是在同一線程中屢次請求同一資源時候出現的問題。

  1. Python遞歸鎖RLock

爲了支持在同一線程中屢次請求同一資源,python提供了"遞歸鎖":threading.RLock。RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源能夠被屢次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多線程的另一種實現方式
        my_thread.start()
複製代碼

上面咱們用一把遞歸鎖,就解決了多個同步鎖致使的死鎖問題。你們能夠把RLock理解爲大鎖中還有小鎖,只有等到內部全部的小鎖,都沒有了,其餘的線程才能進入這個公共資源。

另一點前面沒有就算用類繼承的方法實現python多線程,這個你們能夠查下,就算繼承Thread類,而後從新run方法來實現。

最後你們可能還有個疑問,就算若是咱們都加鎖了,也就是單線程了,那咱們還要開多線程有什麼用呢?這裏解釋下,在訪問共享資源的時候,鎖是必定要存在了,可是咱們的代碼中不是老是在訪問公共資源的,還有一些其餘的邏輯可使用多線程,因此咱們在代碼裏面加鎖的時候,要注意在什麼地方加,對性能的影響最小,這個就靠對邏輯的理解了。

好了今天就說到這裏,我的意見,望指教。

相關文章
相關標籤/搜索