鎖定,共用鎖定,獨占鎖定,死結介紹
訂閱 375
鎖定
鎖定是一種平行狀態下確保業務邏輯正常運轉的機製,其出現的根本原因是平行帶來的執行順序的不確定性,當同一個資源被施加了順序不確定的程式碼邏輯後,出現錯誤將是可以預期的。在不同的系統或語言環境中,鎖定的規則和具體實作會有所不同,這主要取決於需要對資源進行何種形式和程度的存取控製。
雖然規則和具體實作不會完全一致,但鎖定的使用流程是基本相同的,在某些操作執行之前,必須使用一種權杖要求相關鎖定,只有成功取得,操作才能繼續,當操作完成時,持有的鎖定將被釋放。
鎖定和多執行緒之間的關系
如上所述,鎖定之所以被使用是由於對共用資源的平行存取,因此鎖定和多執行緒並沒有必然的聯系,雖然他是鎖定常見的應用案例。當你以某種方式實作了單執行緒中某一資源的平行存取時,鎖定也可以被用在單執行緒中。
獨占鎖定
獨占鎖定只允許被一個目標持有,是拒絕授予共用鎖定或另一個獨占鎖定的依據,當某個權杖被用於獨占鎖定後,不能再使用該權杖申請共用鎖定或另一個獨占鎖定。
由於獨占鎖定僅允許單獨持有,因此在其對應的業務邏輯中執行寫入操作是安全的,當然,先決條件是同一資源的所有寫入操作均運用了鎖定機製。
在下面的 C# 程式碼中,我們將變數locker
作為權杖,並使用獨占鎖定限製變數balance
的存取,當新的執行緒通過函式Pay
修改balance
時,主執行緒呼叫的函式GetBalance
將陷入等候。
// 余額,初始值為 1000
int balance = 1000;
// 用來要求鎖定的權杖
object locker = new();
// 函式 Pay,進行支付操作
void Pay(int amount)
{
// 要求獨占鎖定,確保只有一個目標正在存取 balance
lock (locker)
{
balance -= amount;
// 修改余額後等候 5 秒
Thread.Sleep(5000);
}
}
// 取得目前的余額
int GetBalance()
{
// 要求獨占鎖定,確保只有一個目標正在存取 balance
lock (locker)
return balance;
}
Console.WriteLine("開啟一個執行緒,支付 100");
Thread thread = new(() => Pay(100));
thread.Start();
Console.WriteLine("現在我要檢視余額!");
// 呼叫 GetBalance 將陷入等候,因為此時 Pay 方法持有鎖定
Console.WriteLine($"余額:{GetBalance()}");
開啟一個執行緒,支付 100
現在我要檢視余額!
約 5 秒後…
余額:900
死結
所謂的死結,是指多個目標都在等候對方先釋放持有的鎖定,導致他們均無法繼續執行。在程式碼邏輯可以任意書寫的情況下,死結總是有可能發生,除非設定一些特殊的規則,比如,目標同一時間只能持有一個鎖定,在要求新的鎖定之前,必須釋放舊的鎖定。當然,如果真的按照這種規則來書寫程式碼,那麽某些業務需求將無法被滿足。
在下面的 C# 程式碼中,兩個執行緒在持有自己的獨占鎖定後,開始要求對方持有的鎖定,這導致了死結的發生。
// 用來要求鎖定的權杖
object lockerA = new();
object lockerB = new();
// 執行緒 I 先後使用權杖 lockerA,lockerB 取得獨占鎖定
new Thread(() =>
{
lock (lockerA)
{
Console.WriteLine("執行緒 I 已經取得鎖定 A");
Thread.Sleep(2000);
Console.WriteLine("執行緒 I 嘗試取得鎖定 B");
lock (lockerB)
Console.WriteLine("執行緒 I 已經取得鎖定 B");
}
}).Start();
// 執行緒 II 先後使用權杖 lockerB,lockerA 取得獨占鎖定
new Thread(() =>
{
lock (lockerB)
{
Console.WriteLine("執行緒 II 已經取得鎖定 B");
Thread.Sleep(2000);
Console.WriteLine("執行緒 II 嘗試取得鎖定 A");
lock (lockerA)
Console.WriteLine("執行緒 II 已經取得鎖定 A");
}
}).Start();
執行緒 I 已經取得鎖定 A
執行緒 II 已經取得鎖定 B
約 2 秒後…
執行緒 I 嘗試取得鎖定 B
執行緒 II 嘗試取得鎖定 A