原本在研究設計模式的Singleton,其中提到為了確保唯一性,會在程式內使用關鍵字lock,避免多執行續存取造成非預期的結果。
備註:建議使用英文閱讀MSDN,比較能理解真正的意思。機翻或人工翻譯還是會有不準確的狀況。
Lock
創造一個互斥的程式碼區塊,確保只有同時只有一個執行續在執行lock 內的邏輯。
當其他執行續執行到這個區段時會持續等候,直到lock 被釋放,並在進入lock 區段的同時防止其他Thread 存取。
而lock 所需要監聽的對象,需要是參考型別(記憶體區共用),且極限限制它存取範圍,例如 private static readonly 的物件。
以下為程式由微軟範例修改以更明確展現及解釋lock 的作用:建立一個Account 類別,並對它進行Multi-Thread 的存取,但又能同時確保Thread-Safe,顯示出正確的結果:
public class Account
{
private readonly object balanceLock = new object();
private decimal _balance; // 餘額
public Account(decimal initialValue) => _balance = initialValue;
// 提款
public decimal Debit(int threadId, decimal amount)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount), "提款金額必須大於0");
decimal appliedAmount = 0;
lock (balanceLock)
{
if (_balance >= amount)
{
Console.WriteLine($"第 {threadId} 個執行續正在進行提款 {amount}");
_balance -= amount;
appliedAmount = amount;
Console.WriteLine($"第 {threadId} 個執行續提款 結束");
}
}
return appliedAmount;
}
// 存款
public void Credit(int threadId, decimal amount)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount), "存款金額必須大於0");
lock (balanceLock)
{
Console.WriteLine($"第 {threadId} 個執行續正在進行存款 {amount}");
_balance += amount;
Console.WriteLine($"第 {threadId} 個執行續存款 結束");
}
}
// 查詢餘額
public decimal GetBalance()
{
lock (balanceLock)
{
return _balance;
}
}
}
再來撰寫測試程式:
static async Task Main()
{
// 一開始初始化金額為1000
var account = new Account(1000);
// 同時以3 個執行續進行存提款動作,3 * 2 = 總共六次
var tasks = new Task[3];
for (int i = 0; i < tasks.Length; i++)
{
var threadId = i + 1;
tasks[i] = Task.Run(() => Update(threadId, account));
}
await Task.WhenAll(tasks);
// 每一次Update,最後金額會+100
// 故最後總金額:1000 + ( 100 * 3 ) = 1300
Console.WriteLine($"總金額:{account.GetBalance()}");
Console.ReadLine();
}
static void Update(int threadId, Account account)
{
// 連續進行存提款動作
decimal[] amounts = { 150, -50 };
foreach (var amount in amounts)
{
if (amount >= 0)
account.Credit(threadId, amount);
else
account.Debit(threadId, Math.Abs(amount));
}
// 做完總共 +100
}
以下解說執行結果:
Update 裡面是連續進行"存款"和"提款"兩個動作,Row 1, 2 存款執行完後應該要立刻進行Row 7, 8 的提款動作。
但是因為有使用到lock,準備要進行提款時發現已經被lock 住,所以一定要等到前面的thread 結束釋放lock 之後,下一個thread 才會進行動作。
那如果把lock 拿掉呢?
是不是發現金額不對了
關鍵在於當Row1, 2 同時進行卻沒有lock 金額,造成以下狀況:
兩條時間線同時進行
1:Thread 2 讀出目前金額 1000
2:Thread 1 讀出目前金額 1000(x)
Row 2 的thread 1 沒有等到thread 2 完成存款動作,就直接拿現有的金額1000 來處理
3:Thread 2 存款150 -> 把金額變 1000 + 150 = 1150
4:Thread 1 存款150 -> 把金額變 1000(x) + 150 = 1150(x)
又因為沒有lock,Row 4 thread 1不管 thread 2 的結果,直接把金額變成1150
你的存款就硬生生地少了150 喔,這個範例體現了Singleton 單一實體時,lock 的影響是多巨大了。
故在這種特定屬性或物件,一次只能有一個人存的情況時,可以採用lock
參考資料:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/statements/lock
https://createps.pixnet.net/blog/post/32352527-c%23-lock-record