本文通過程式碼實操講解了如何使用 python 實現簡單的共享鎖和排他鎖。
上篇文章回顧:記一次容量提升5倍的HttpDns業務Cache調優
共享鎖和排它鎖
1、什麼是共享鎖
共享鎖又稱為讀鎖。
從多執行緒的角度來講,共享鎖允許多個執行緒同時訪問資源,但是對寫資源只能又一個執行緒進行。
從事務的角度來講,若事務 T 對資料 A 加上共享鎖,則事務 T 只能讀 A; 其他事務也只能對資料 A 加共享鎖,而不能加排他鎖,直到事務 T 釋放 A 上的 S 鎖。這就保證了其他事務可以讀 A,但是在事務 T 釋放 A 上的共享鎖之前,不能對 A 做任何修改。
2、什麼是排它鎖
排他鎖又成為寫鎖。
從多執行緒的角度來講,在訪問共享資源之前對進行加鎖操作,在訪問完成之後進行解鎖操作。 加鎖後,任何其他試圖再次加鎖的執行緒會被阻塞,直到當前程式解鎖。如果解鎖時有一個以上的執行緒阻塞,那麼所有該鎖上的執行緒都被程式設計就緒狀態, 第一個變為就緒狀態的執行緒又執行加鎖操作,那麼其他的執行緒又會進入等待。 在這種方式下,只有一個執行緒能夠訪問被互斥鎖保護的資源。
從事務的角度來講,若事務T對資料物件A加上排它鎖,則只允許T讀取和修改資料A,其他任何事務都不能再對A加任何型別的鎖,直到事務T釋放X鎖。它可以防止其他事務獲取資源上的鎖,直到事務末尾釋放鎖。
InnoDB 中的行鎖
InnoDB實現了以下兩種型別的行鎖:
共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖。
排他鎖(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實現
1、程式碼實現
不多說,直接上程式碼:
# -*- 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()複製程式碼
2、實現思路
以 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 鎖解鎖。
3、鎖相容測試
測試程式碼
# -*- 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 鎖 沒有衝突!複製程式碼
4、公平鎖與非公平鎖
(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
文章首發於共公眾號“小米運維”,點選檢視原文。