併發編程的藝術03-Bakery互斥鎖算法





導讀


本章會介紹Bakery互斥鎖算法,涉及到併發下的公平性問題,有界計數器和無界計數器問題, 存儲單元數量下界問題。



公平性


無飢餓特性能夠保證每一個調用 lock() 函數的線程最終都將進入臨界區,但並不能保證進入臨界區需要多長時間。理想情況下如果線程 A 在線程 B 之前調用 lock() 函數,那麼 A 應該在 B 之前進入臨界區。然而,運用現有的工具無法確定那個線程首先調用 lock() 函數。取而代之的做法是,將 lock() 函數代碼劃分爲兩個部分:
1. 門廊區:其執行區間由有限個操作步組成。
2. 等待區:其執行區間可能包擴無窮個操作步。


門廊區應該在有限步數內完成一種強約束條件。稱這種約束爲有界無等待演進特性。對於公平的定義:滿足下麪條件的鎖稱爲先到先服務的:如果線程 A 門廊區的結束在線程 B 門廊區的開始之前完成,那麼線程 A 比定不會被線程 B 趕超。


按照我們的慣例來舉一個生活中的例子來幫助讀者理解這種計算機術語都抽象描述。





大多數人都去銀行辦理過業務,如圖1所示很多人都在等待,他們等待的依據是什麼呢?總得有個先來後到吧,要不然有人插隊豈不是要發生爭吵了。於是銀行想了一個辦法給每一個來辦理業務的顧客發一個號碼,這個號碼就是大家排隊的依據。銀行按照先到先服務(First-Come-First-Served)(這裏的“先到”指的是誰先獲取到號碼而不是誰先進入銀行)的準則來控制當前該叫到那個號碼的持有者來辦理業務。這種做法就是一種保障公平性的機制。在這個例子中銀行中的取號機可以抽象爲前文提到的"門廊區",而客戶坐在椅子上等待可以抽象爲前文提到的"等待區"。









Bakery 算法


在瞭解了公平性之後對 Bakery 算法就很容易理解了,因爲 Bakery 保證公平性的方式和前文中舉的銀行排號例子原理是一樣的。每個線程在門廊區得到一個序號,然後一直等待,直到再也沒有序號比自己更早的線程嘗試進入臨界區止。

該算法中 flag[A] 是一個布爾型標誌,表示線程 A 是否想要進入臨界區;

lable[A] 是一個整數型,說明線程進入麪包店的相對次數。

Bakery 算法是無死鎖的,正在等待的線程中,比定存在某一個線程 A具有最小的 lable[A] ,那麼這個線程絕不會等待其他線程。
注意,既然滿足無死鎖又滿足先到先服務特性的算法必定是無飢餓的。


class BakeryLock implements Lock {    private boolean[] flag;    private int[] label;    private int n;
public BakeryLock(int n) { this.n = n; flag = new boolean[n]; label = new int[n]; for (int i = 0;i < n; i++) { flag[i] = false; label[i] = 0; } }
public void lock() { int i = ThreadID.get(); flag[i] = true; label[i] = max(label) + 1; for (int k = 0; k < n; k++) { while ((k != i) && flag[k] && ((label[k] < label[i]) || ((label[k] == label[i]) && k < i))) {
} } }
public void unlock() { flag[ThreadID.get()] = false; }
private int max(int[] elementArray) { int maxValue = Integer.MIN_VALUE; for (int element : elementArray) { if (element > maxValue) { maxValue = element; } } return maxValue; }}










有界計數器和無界計數器


在理解了 Bakery 算法後,我們再來仔細看看這個算法中的問題。首先就是存在的一個 bug ,就是 label[i] 的值會出現溢出的可能



lable 值是無限增長的,因此在生命週期很長的系統中不得不考慮溢出的問題。如果某個線程的 lable 在其他線程都不知情的情況下從一個很大的數返回到 0 ,那麼公平性將被破壞。例如到2038年1月18日,Unix 的 time_t 數據結構將會溢出,因爲其秒數值是從 1970 年 1 月開始計算的,而在那一刻將會超過 2 的 32 次方。大多數採用 64-bit 計數器的應用程序在其聲明週期內是不可能發生這種“回零”問題的。

Bakery 算法保證公平性的做法是確保某個線程在另一個線程之前得到一個 lable 值,那麼後一個線程的 lable 值一定比前者大。通過仔細觀察 Bakery 算法代碼,我們可以得出一個線程需要具備兩種能力:

1. 讀取其他線程的 lable (掃描)。

2. 爲自己設置一個更大的 lable (標記)。


這時候的 Bakery 算法中的 lable 值獲取看起來像是這樣:這個數是隨着時間無限向後增長的,顯然它是無限的 ,直到出現溢出問題。



爲了解決這個溢出問題我們考慮使用有界的 lable 值獲取,類似這樣(這是隻有兩個線程的情況):



在這個有向環中是一系列的節點 n0 , n1 , ... , nk ,其中有一條邊從 n0到n1,有一條邊從n1到n2,最後一條邊從n(k - 1) 到 nk ,並有一條邊從nk返回n0。邊定義結果集上的次序關係爲:0 < 1 , 1 < 2 , 2 < 0。兩個線程的 lable 在 0 , 1 , 2 三個節點中不斷的輪轉改變。

N 個 線程的情況較爲複雜暫時不進行討論,只是說明結論。









存儲單元數量下界


還記得我們之前說過的麼,會介紹一些經典但是不實用的互斥鎖算法,Bakery算法就是其中之一。及時 Bakery 算法十分的簡潔,無飢餓,無死鎖,而且公平,那麼它爲什麼不實用呢?最主要的問題是要讀寫 N 個不同的存儲單元。(N 是線程的最大個數)


那麼是否有更好的基於讀/寫存儲器的鎖算法可以避免這種開銷呢?答案是否定的。也就是說任何一種無死鎖的鎖算法在最壞情況下至少需要讀/寫 N 個不同的存儲單元。正是因爲如此,才促使我們的多處理器計算機中,增加了一些功能要比讀/寫更強大的同步操作,並以這些操作作爲互斥算法的基礎。


現在我們要說明爲什麼這種線性下界是解決互斥問題時鎖固有的。要記得一點只能通過讀/寫指令訪問存儲單元具有一個重要限制:一個線程向某個指定單元寫的任何信息,在其他線程讀取之前可能會被覆蓋。下面是證明過程












到這裏Bakery算法就講完了,希望對你有幫助,如果覺得有收穫還請點擊"再看"自持作者。本人才疏學淺,如果文中有不當之處還請留言指正。



精彩內容回顧

《幸併發編程的藝術02-過濾鎖算法》

  ...


《併發編程01-面試中被問到併發基礎知識答不上來?》

 ...


《一文讓你讀懂Dubbo中的SPI擴展機制》

 ......


《走進Java Volatile關鍵字》

 ......










本文分享自微信公衆號 - 黑帽子技術(SNJYYNJY2020)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章