本文經過代碼實操講解了如何使用 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實現瞭如下兩種類型的行鎖:運維
共享鎖(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 方式得到排他鎖。
很少說,直接上代碼:
# -*- 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
文章首發於共公衆號「小米運維」,點擊查看原文。