Python | 淺談併發鎖與死鎖問題

本文始發於我的公衆號:TechFlow,原創不易,求個關注程序員


今天是Python專題的第24篇文章,咱們一塊兒來聊聊多線程場景當中不可或缺的另一個部分——web

若是你學過操做系統,那麼對於鎖應該不陌生。鎖的含義是線程鎖,能夠用來指定某一個邏輯或者是資源同一時刻只能有一個線程訪問。這個很好理解,就好像是有一個房間被一把鎖鎖住了,只有拿到鑰匙的人才能進入。每個人從房間門口拿到鑰匙進入房間,出房間的時候會把鑰匙再放回到門口。這樣下一個到門口的人就能夠拿到鑰匙了。這裏的房間就是某一個資源或者是一段邏輯,而拿取鑰匙的人其實指的是一個線程。編程

加鎖的緣由

咱們明白了鎖的原理,不由有了一個問題,咱們爲何須要鎖呢,它在哪些場景當中會用到呢?多線程

其實它的使用場景很是廣,咱們舉一個很是簡單的例子,就是淘寶買東西。咱們都知道商家的庫存都是有限的,賣掉一個少一個。假如說當前某個商品庫存只剩下一個,但當下卻有兩我的同時購買。兩我的同時購買也就是有兩個請求同時發起購買請求,若是咱們不加鎖的話,兩個線程同時查詢到商品的庫存是1,大於0,進行購買邏輯以後,減一。因爲兩個線程同時執行,因此最後商品的庫存會變成-1。併發

顯然商品的庫存不該該是一個負數,因此咱們須要避免這種狀況發生。經過加鎖能夠完美解決這個問題。咱們規定一次只能有一個線程發起購買的請求,那麼這樣當一個線程將庫存減到0的時候,第二個請求就沒法修改了,就保證了數據的準確性。編輯器

代碼實現

那麼在Python當中,咱們怎麼樣來實現這個鎖呢?分佈式

其實很簡單,threading庫當中已經爲咱們提供了線程的工具,咱們直接拿過來用就能夠了。咱們經過使用threading當中的Lock對象, 能夠很輕易的實現方法加鎖的功能。工具

import threading

class PurchaseRequest:
    '''
    初始化庫存與鎖
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加庫存
        '''

        self._lock.acquire()
        self._value += delta
        self._lock.release()

    def decr(self,delta=1):
        '''
        減庫存
        '''

        self._lock.acquire()
        self._value -= delta
        self._lock.release()

咱們從代碼當中就能夠很輕易的看出Lock這個對象的使用方法,咱們在進入加鎖區(資源搶佔區)以前,咱們須要先使用lock.acquire()方法獲取鎖。Lock對象能夠保證同一時刻只能有一個線程獲取鎖,只有獲取了鎖以後纔會繼續往下執行。當咱們執行完成以後,咱們須要把鎖「放回門口」,因此須要再調用一下release方法,表示鎖的釋放。測試

這裏有一個小問題是不少程序員在編程的時候老是會忘記release,致使沒必要要的bug,並且這種分佈式場景當中的bug很難經過測試發現。由於測試的時候每每很難測試併發場景,code review的時候也很容易忽略,所以一旦泄露了仍是挺難發現的。ui

爲了解決這個問題,Lock還提供了一種改進的用法,就是使用with語句。with語句咱們以前在使用文件的時候用到過,使用with能夠替咱們完成try catch以及資源回收等工做,咱們只管用就完事了。這裏也是同樣,使用with以後咱們就能夠不用管鎖的申請和釋放了,直接寫代碼就行,因此上面的代碼能夠改寫成這樣:

import threading

class PurchaseRequest:
    '''
    初始化庫存與鎖
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加庫存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        減庫存
        '''

        with self._lock:
         self._value -= delta

這樣看起來是否是清爽不少?

可重入鎖

上面介紹的只是最簡單的鎖,咱們常用的每每是可重入鎖

什麼叫可重入鎖呢?簡單解釋一下,就是在一個線程已經持有了鎖的狀況下,它能夠再次進入被加鎖的區域。可是既然線程還持有鎖沒有釋放,那麼它不該該仍是在加鎖區域嗎,怎麼會有須要再次進入被加鎖區域的狀況呢?實際上是有的,道理也很簡單,就是遞歸

咱們把上面的例子稍微改一點點,就徹底不同了。

import threading

class PurchaseRequest:
    '''
    初始化庫存與鎖
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加庫存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        減庫存
        '''

        with self._lock:
         self.incr(-delta)

咱們關注一下上面的decr方法,咱們用incr來代替了本來的邏輯實現了decr。可是有一個問題是decr也是一個加鎖的方法,須要前一個鎖釋放了才能進入。但它已經持有了鎖了,那麼這種狀況下就會發生死鎖

咱們只須要把Lock換成可重入鎖就能夠解決這個問題,只須要修改一行代碼。

import threading

class PurchaseRequest:
    '''
    初始化庫存與鎖
    咱們使用RLock代替了Lock,也可重入鎖代替了普通鎖
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.RLock()

    def incr(self,delta=1):
        '''
        加庫存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        減庫存
        '''

        with self._lock:
         self.incr(-delta)

總結

今天咱們的文章介紹了Python當中鎖的使用方法,以及可重入鎖的概念。在併發場景下開發和調試都是一個比較困難的工做,稍微不當心就會踩到各類各樣的坑,死鎖只是其中一種比較常見而且比較容易解決的問題,除此以外還有不少其餘各類各樣的問題。

針對死鎖的問題,Python還提供了其餘的解決方案,咱們放到下一篇文章當中再和你們分享。

今天的文章到這裏就結束了,若是喜歡本文的話,請來一波素質三連,給我一點支持吧(關注、轉發、點贊)。

- END -

相關文章
相關標籤/搜索