使用 python 實現簡單的共享鎖和排他鎖

本文經過代碼實操講解了如何使用 python 實現簡單的共享鎖和排他鎖。
上篇文章回顧: 記一次容量提高5倍的HttpDns業務Cache調優

共享鎖和排它鎖

一、什麼是共享鎖

共享鎖又稱爲讀鎖。
html

從多線程的角度來說,共享鎖容許多個線程同時訪問資源,可是對寫資源只能又一個線程進行。python

從事務的角度來說,若事務 T 對數據 A 加上共享鎖,則事務 T 只能讀 A; 其餘事務也只能對數據 A 加共享鎖,而不能加排他鎖,直到事務 T 釋放 A 上的 S 鎖。這就保證了其餘事務能夠讀 A,可是在事務 T 釋放 A 上的共享鎖以前,不能對 A 作任何修改。編程

二、什麼是排它鎖

排他鎖又成爲寫鎖。bash

從多線程的角度來說,在訪問共享資源以前對進行加鎖操做,在訪問完成以後進行解鎖操做。 加鎖後,任何其餘試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。若是解鎖時有一個以上的線程阻塞,那麼全部該鎖上的線程都被編程就緒狀態, 第一個變爲就緒狀態的線程又執行加鎖操做,那麼其餘的線程又會進入等待。 在這種方式下,只有一個線程可以訪問被互斥鎖保護的資源。多線程

從事務的角度來說,若事務T對數據對象A加上排它鎖,則只容許T讀取和修改數據A,其餘任何事務都不能再對A加任何類型的鎖,直到事務T釋放X鎖。它能夠防止其餘事務獲取資源上的鎖,直到事務末尾釋放鎖。app

InnoDB 中的行鎖

InnoDB實現瞭如下兩種類型的行鎖:運維

共享鎖(S):容許一個事務去讀一行,阻止其餘事務得到相同數據集的排他鎖。 post

排他鎖(X):容許得到排他鎖的事務更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖。性能

另外,爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB 還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。測試

意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖。

意向排他鎖(IX)事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的 IX 鎖。

若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB 就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。

意向鎖是 InnoDB 自動加的,不需用戶干預。對於 UPDATE、DELETE 和 INSERT 語句,InnoDB 會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB 不會加任何鎖;事務能夠經過如下語句顯示給記錄集加共享鎖或排他鎖。

共享鎖(S):

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE複製代碼

排他鎖(X):

SELECT * FROM table_name WHERE ... FOR UPDATE複製代碼

用 SELECT ... IN SHARE MODE得到共享鎖,主要用在須要數據依存關係時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操做。可是若是當前事務也須要對該記錄進行更新操做,則頗有可能形成死鎖,對於鎖定行記錄後須要進行更新操做的應用,應該使用 SELECT... FOR UPDATE 方式得到排他鎖。

使用Python實現

一、代碼實現

很少說,直接上代碼:

# -*- coding: utf-8 -*-import threadingclass Source: # 隊列成員標識 __N = None # 排他鎖 __X = 0 # 意向排他鎖 __IX = 1 # 共享鎖標識 __S = 2 # 意向共享標識 __IS = 3 # 同步排他鎖 __lockX = threading.Lock() # 事件通知 __events = [ threading.Event(), threading.Event(), threading.Event(), threading.Event() ] # 事件通知隊列 __eventsQueue = [ [], [], [], [] ] # 事件變動鎖 __eventsLock = [ threading.Lock(), threading.Lock(), threading.Lock(), threading.Lock() ] # 相互互斥的鎖 __mutexFlag = {} # 鎖類 class __ChildLock: # 鎖標識 __flag = 0 # 鎖定的資源 __source = None def __init__(self, source, flag): self.__flag = flag self.__source = source # 加鎖 def lock(self): self.__source.lock(self.__flag) # 解鎖 def unlock(self): self.__source.unlock(self.__flag) def __init__(self): self.__initMutexFlag() self.__initEvents() # 不建議直接在外面使用,以避免死鎖 def lock(self, flag): # 若是是排他鎖,先進進行枷鎖 if flag == self.__X: self.__lockX.acquire() self.__events[flag].wait() self.__lockEvents(flag) # 不建議直接在外面使用,以避免死鎖 def unlock(self, flag): self.__unlockEvents(flag) if flag == self.__X: self.__lockX.release() # 獲取相互互斥 def __getMutexFlag(self, flag): return self.__mutexFlag[flag] def __initMutexFlag(self): self.__mutexFlag[self.__X] = [self.__X, self.__IX, self.__S, self.__IS] self.__mutexFlag[self.__IX] = [self.__X, self.__S] self.__mutexFlag[self.__S] = [self.__X, self.__IX] self.__mutexFlag[self.__IS] = [self.__X] def __initEvents(self): for event in self.__events: event.set() # 給事件加鎖, 調用 wait 時阻塞 def __lockEvents(self, flag): mutexFlags = self.__getMutexFlag(flag) for i in mutexFlags: # 爲了保證原子操做,加鎖 self.__eventsLock[i].acquire() self.__eventsQueue[i].append(self.__N) self.__events[i].clear() self.__eventsLock[i].release() # 給事件解鎖, 調用 wait 不阻塞 def __unlockEvents(self, flag): mutexFlags = self.__getMutexFlag(flag) for i in mutexFlags: # 爲了保證原子操做,加鎖 self.__eventsLock[i].acquire() self.__eventsQueue[i].pop(0) if len(self.__eventsQueue[i]) == 0: self.__events[i].set() self.__eventsLock[i].release() # 獲取鎖 def __getLock(self, flag): lock = self.__ChildLock(self, flag) lock.lock() return lock # 獲取 X 鎖 def lockX(self): return self.__getLock(self.__X) # 獲取 IX 鎖 def lockIX(self): return self.__getLock(self.__IX) # 獲取 S 鎖 def lockS(self): return self.__getLock(self.__S) # 獲取 IS 鎖 def lockIS(self): return self.__getLock(self.__IS)複製代碼

使用方式:

from lock import Source# 初始化一個鎖資源source = Source()# 獲取資源的X鎖,獲取不到則線程被阻塞,獲取到了繼續往下執行lock = source.lockX() lock.unlock()lock = source.lockIX() lock.unlock()lock = source.lockS() lock.unlock()lock = source.lockIS() lock.unlock()複製代碼

二、實現思路

以 S 鎖爲例,獲取鎖的步驟以下:

  • 檢測 S 鎖是否能夠取到,取到了話繼續執行,沒有取到則阻塞,等待其餘線程解鎖喚醒。

  • 獲取與 S 鎖相互衝突的鎖(IX,X),並將 IX 鎖和 X 鎖 鎖住,後續想得到 IX 鎖或者 X 鎖的線程就會被阻塞。

  • 向 IX 鎖和 X 鎖的標識隊列插入標識,若是此時另一個線程拿到了 IS 鎖,則會繼續想 IX 鎖隊列標識插入標識。

  • 完成加鎖,返回 S 鎖。

以 S 鎖爲例,解鎖的步驟以下:

  • 獲取與 S 鎖相互衝突的鎖(IX,X),向 IX 鎖和 X 鎖的標識隊列移除一個標識。

  • 判斷 IX 鎖和 X 鎖隊列標識是否爲空,若是不爲空,則繼續鎖定,爲空則解鎖並喚醒被 IX 鎖和 X 鎖阻塞的線程。

  • 完成 S 鎖解鎖。

三、鎖兼容測試

測試代碼

# -*- coding: utf-8 -*-import threadingimport timefrom lock import Source# 初始化資源source= Source()maplockname=['X','IX','S','IS']class MyThread(threading,Thread): flag = None def __init__(self, flag): super().__init__() self.flag = flag def run(self): lock = self.lock() time1 = time.time() strtime1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time1)) print('我拿到 %s 鎖,開始執行了喔,如今時間是 %s' % (maplockname[self.flag], strtime1)) time.sleep(1) time2 = time.time() strtime2 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time2)) print('我釋放 %s 鎖,結束執行了,如今時間是 %s' % (maplockname[self.flag], strtime2)) lock.unlock() def lock(self): if self.flag == 0: return source.lockX() elif self.flag == 1: return source.lockIX() elif self.flag == 2: return source.lockS() else: return source.lockIS() def unlock(self, lock): lock.unlock()def test_lock(): for x in range(0, 4): for y in range(0, 4): time1 = time.time() thread1 = MyThread(x) thread2 = MyThread(y) thread1.start() thread2.start() thread1.join() thread2.join() time2 = time.time() difftime = time2 - time1 if difftime > 2: print('%s 鎖和 %s 鎖 衝突了!' % (maplockname[x], maplockname[y])) elif difftime > 1: print('%s 鎖和 %s 鎖 沒有衝突!' % (maplockname[x], maplockname[y])) print('')if __name__ == '__main__': test_lock()複製代碼

運行結果:

我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:09我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:10我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:10我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:11X 鎖和 X 鎖 衝突了!我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:11我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:12我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:12我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:13X 鎖和 IX 鎖 衝突了!我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:13我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:14我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:14我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:15X 鎖和 S 鎖 衝突了!我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:15我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:16我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:16我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:17X 鎖和 IS 鎖 衝突了!我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:17我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:18我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:18我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:19IX 鎖和 X 鎖 衝突了!我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:19我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:19我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:20我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:20IX 鎖和 IX 鎖 沒有衝突!我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:20我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:21我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:21我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:22IX 鎖和 S 鎖 衝突了!我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:22我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:22我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:23我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:23IX 鎖和 IS 鎖 沒有衝突!我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:23我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:24我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:24我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:25S 鎖和 X 鎖 衝突了!我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:25我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:26我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:26我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:27S 鎖和 IX 鎖 衝突了!我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:27我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:27我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:28我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:28S 鎖和 S 鎖 沒有衝突!我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:28我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:28我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:29我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:29S 鎖和 IS 鎖 沒有衝突!我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:29我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:30我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:30我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 18:38:31IS 鎖和 X 鎖 衝突了!我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:31我拿到 IX 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:31我釋放 IX 鎖了,結束執行了,如今時間是 2019-02-17 18:38:32我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:32IS 鎖和 IX 鎖 沒有衝突!我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:32我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:32我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:33我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 18:38:33IS 鎖和 S 鎖 沒有衝突!我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:33我拿到 IS 鎖了,開始執行了喔,如今時間是 2019-02-17 18:38:33我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:34我釋放 IS 鎖了,結束執行了,如今時間是 2019-02-17 18:38:34IS 鎖和 IS 鎖 沒有衝突!複製代碼

四、公平鎖與非公平鎖

(1)問題分析

仔細想了想,若是有一種場景,就是用戶一直再讀,寫獲取不到鎖,那麼不就形成髒讀嗎?這不就是因爲資源的搶佔不就是非公平鎖形成的。如何避免這個問題呢?這就涉及到了公平鎖與非公平鎖。

對產生的結果來講,若是一個線程組裏,能保證每一個線程都能拿到鎖,那麼這個鎖就是公平鎖。相反,若是保證不了每一個線程都能拿到鎖,也就是存在有線程餓死,那麼這個鎖就是非公平鎖。

(2)非公平鎖測試

上述代碼鎖實現的是非公平鎖,測試代碼以下:

def test_fair_lock():    threads = []        for i in range(0, 10):                  if i == 2:                       # 0 表明排他鎖(X) threads.append(MyThread(0)) else: # 2 表明共享鎖(S) threads.append(MyThread(2)) for thread in threads: thread.start()複製代碼

運行結果:

我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:33我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:06:34我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 19:06:34我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 19:06:35複製代碼

能夠看到因爲資源搶佔問題,排他鎖被最後才被獲取到了。

(3)公平鎖的實現

實現公平鎖,只須要在原有的代碼進行小小得修改就好了。

class Source:    # ...... 省略 def __init__(self, isFair=False): self.__isFair = isFair self.__initMutexFlag() self.__initEvents() # ...... 省略 def lock(self, flag): # 若是是排他鎖,先進進行枷鎖 if flag == self.__X: self.__lockX.acquire() if self.__isFair: # 若是是公平鎖則,先將互斥的鎖給阻塞,防止其餘線程進入 self.__lockEventsWait(flag) self.__events[flag].wait() self.__lockEventsQueue(flag) else: # 若是是非公平鎖,若是鎖拿不到,則先等待 self.__events[flag].wait() self.__lockEvents(flag) def __lockEventsWait(self, flag): mutexFlags = self.__getMutexFlag(flag) for i in mutexFlags: # 爲了保證原子操做,加鎖 self.__eventsLock[i].acquire() self.__events[i].clear() self.__eventsLock[i].release() def __lockEventsQueue(self, flag): mutexFlags = self.__getMutexFlag(flag) for i in mutexFlags: # 爲了保證原子操做,加鎖 self.__eventsLock[i].acquire() self.__eventsQueue[i].append(self.__N) self.__eventsLock[i].release()複製代碼

測試代碼:

source = Source(True)def test_fair_lock():    threads = []        for i in range(0, 10):                  if i == 2:                            # 0 表明排他鎖(X) threads.append(MyThread(0)) else: # 2 表明共享鎖(S) threads.append(MyThread(2)) for thread in threads: thread.start()複製代碼

運行結果:

我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:16我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:16我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:17我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:17我拿到 X 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:17我釋放 X 鎖了,結束執行了,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我拿到 S 鎖了,開始執行了喔,如今時間是 2019-02-17 19:35:18我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19我釋放 S 鎖了,結束執行了,如今時間是 2019-02-17 19:35:19複製代碼

能夠看到排他鎖在第二次的時候就被獲取到了。

(4)優缺點

非公平鎖性能高於公平鎖性能。首先,在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲。並且,非公平鎖能更充分的利用cpu的時間片,儘可能的減小cpu空閒的狀態時間。

參考文獻:

共享鎖(S鎖)和排它鎖(X鎖):https://www.jianshu.com/p/bd3b3ccedda9

Java多線程--互斥鎖/共享鎖/讀寫鎖 快速入門:https://www.jianshu.com/p/87ac733fda80

Java多線程 -- 公平鎖和非公平鎖的一些思考:https://www.jianshu.com/p/eaea337c5e5b

MySQL- InnoDB鎖機制:https://www.cnblogs.com/aipiaoborensheng/p/5767459.html


文章首發於共公衆號「小米運維」,點擊查看原文

相關文章
相關標籤/搜索