【趣話計算機底層技術】一個故事看懂各種鎖

軒轅之風發表於2023-05-17

我是一個執行緒,一個賣票程式的執行緒。

自從我們執行緒誕生以來,同一個程式地址空間裡允許有多個執行流一起執行,效率提升的同時,也引來了很多麻煩。

我們賣票執行緒的工作很簡單,比如票的總數是100,每賣一張就減1,直到變成0售完為止。

以前單執行緒的時候沒啥問題,但多個執行緒一起執行的時候就發現,有些傢伙讀取到票數是100,減1後變成99,還沒等他把票數寫回去,另外有別的執行緒去讀也是100,也做了同樣的事,結果賣了兩張票,票數才減了1張,一天下來,多賣了很多票,氣的人類差點想砸了我們。

原子操作

我們把這問題反饋給了作業系統大哥,他給我們的解決方案是:讀取票數->票數減1->寫回票數這三個步驟不能被拆分,中途不能被打斷,他說這個叫原子操作

他給了我們一套原子操作的手冊,裡面不止有減法,還有加法、位運算,只要呼叫手冊裡的原子操作函式,就能保證邏輯的正確性。

我很好奇作業系統大哥是如何實現這個過程的原子化的,他告訴我,如果CPU只有一個核很好辦,執行原子操作的時候,他不切換執行緒就可以。而如果是有多個核,就需要CPU來幫忙了。

你還別說,我們用這個原子操作來賣票後,再也沒有發生超額賣票的問題。

自旋鎖

有一天,我們賣票程式進行了升級,不再是直接讀取票數->票數減1->寫回票數這麼簡單,還需要安排座位,現在變成了:

 我們一翻手冊,沒有哪個原子操作函式能滿足我們的功能,畢竟安排座位這個操作是我們們賣票程式自己的事兒,一點也不通用,作業系統大哥肯定不會專門為我們開發一個原子操作函式。

我們只好再一次求助作業系統大哥,他一看就說:“你們這個問題,用自旋鎖就可以解決”

鎖?我們還是第一次聽說這玩意,不知道是什麼意思。

作業系統告訴我們,讓我們回去建立一個鎖,這鎖裡面有一個狀態標記來表示當前有沒有被佔用,所有執行緒在執行賣票操作之前,都得先去獲取這個鎖,如果鎖被佔用了,執行緒就會阻塞在獲取函式那裡,獲取函式內部會不斷迴圈去檢查,直到別的執行緒釋放後才返回。

 因為獲取鎖的時候執行緒會一直迴圈檢查狀態,所以這鎖也叫自旋鎖。

現在,我們的工作流程變成了:

我們又可以愉快地賣票了!

互斥鎖

我們的業務發展很快,後來,我們用上了資料庫,把票的數量寫到了資料庫裡面,於是我們的工作流程變成了:

 本以為只是把票數從本地記憶體搬到了資料庫,應該沒什麼不一樣,結果發現我們執行經常出錯,還莫名其妙地被殺掉程式。

我們向作業系統大哥大倒苦水,沒想到他卻說:“你們還好意思訴苦,你們獲取自旋鎖後搞那麼耗時的操作,讓別的執行緒一直自旋等待,把CPU都跑得飛起,風扇都轉個不停···”

我們都羞愧地低下了頭,原來,把票數從本地記憶體搬到了資料庫,差別這麼大。

作業系統又接著說道:“自旋鎖因為會使得執行緒一直阻塞自旋,沒有讓出CPU,所以只適合處理比較快速的場合,像讀取資料庫這種很耗時的操作,不能用它,會白白浪費CPU時間!”

我們又詢問:“有沒有別的不浪費CPU的辦法呢?”

作業系統大哥又給我們介紹了一個叫互斥鎖的東西,聽說獲取這個鎖的時候,執行緒不會去自旋檢查,而是把自己放到這把鎖的等待佇列中,然後就交出CPU執行許可權,進入睡眠,看起來就跟阻塞一樣,等到後面別的執行緒釋放鎖之後,再去喚醒它的等待佇列裡的執行緒繼續執行。

回去以後我們就用上了互斥鎖,現在我們的流程變成了這樣:

 我們又又能愉快地賣票了!

條件變數

有一天,我們的賣票程式又進行了升級,21-100號票價格比較便宜,交給其他執行緒來賣,1-20號票價格比較貴,交給我來賣。

現在,我們不同的執行緒賣的票不一樣了。

別的執行緒的流程是這樣:

 而我的流程是這樣:

 使用互斥鎖倒也沒什麼問題,可就是我經常拿到鎖以後發現票號還大於20,不該我處理,只好默默的釋放鎖,白白把我喚醒,卻什麼也沒幹!

空手而回的次數多了以後,我又去請教作業系統大哥,能不能讓我指定一個條件,等條件滿足了再喚醒我執行,別讓我白跑。

沒想到還真有辦法!作業系統告訴了我一個叫條件變數的東西,等待條件變數的執行緒平時阻塞著,別的執行緒發現條件滿足之後,就將條件變數啟用,那個時候等待的執行緒才會被喚醒。

回去之後,我跟我的小夥伴兒們商量了一下,我們建立了一個條件變數,等到它們發現票號小於等於20的時候,就把條件變數啟用,我就會被喚醒,再也不用白跑了!

訊號量

互斥鎖和條件變數真是好東西,幫了我們大忙,不僅幫我們解決了賣票的問題,我們還在其他很多地方使用它,我們遇到的絕大多數同步和互斥問題都可以用它們來解決。

直到有一天,我們遇到了一個新的問題。

我們的票越賣越好,從100到1000,票的數量越來越多,來找我們買票的客戶也越來越多。

因為每次售票都要訪問資料庫,連線它的執行緒有些多,那傢伙有些吃不消了。

希望我們控制一下訪問資料庫的執行緒數量。

我們很自然的想到了互斥鎖,只有拿到鎖的執行緒才能去訪問資料庫。

可這互斥鎖名叫互斥,只能允許一個執行緒拿到鎖,總不能只允許一個執行緒訪問資料庫吧,那可不行。所以我們希望這個名額能放寬,允許多個執行緒同時獲得鎖。

我們再一次找到了作業系統大哥,大哥拿出了他的絕招——訊號量

他告訴我們,這訊號量就像一個升級版的互斥鎖,它裡面有一個計數器,可以用來指定最多允許多少個執行緒同時獲得它。

這正是我們想要的鎖!

很快我們用上了訊號量,我們又又又能愉快地賣票了!

Tips:在訊號量一節中,實際上資料庫能承受的併發量遠不止這點,這裡為了故事情節需要,弱化了資料庫的併發承受能力。

往期推薦

相關文章