[.NET] 併發基本三原則原子性、可見性、有序性
在設計併發(Concurrency)程式時往往忽略基本三原則原子性(Atomic)、可見性(Visibility)、有序性(Ordering),可能在程式執行時造成非預期的錯誤,透過下面介紹來瞭解這些原則因而避免錯誤產生。
原子性 (Atomic)
原子性就是指一個或是多個操作,不會因為受到外部影響造成中斷,如果中斷就全部不執行。
- 所有操作的動作或變動都完成 (全部成功)
- 操作中斷所有操作的動作或變動都不存在,等於沒有改變 (全部失敗)
銀行轉帳範例
從帳戶 A 轉一千元進帳戶 B,勢必有兩個動作:
- 從帳戶 A 扣一千元
- 帳戶 B 增加一千元
所以如果從帳戶 A 扣一千元,之後流程突然被中斷,這就會發生帳戶 B 都沒收到帳戶 A 的一千元,這一千元就憑空消失了,如果在現實中發生客戶勢必會急著跳腳。
原子性範例
class Program
{
static async Task Main(string[] args)
{
var accountA = new BankAccount();
accountA.Balance = 10000;
var accountB = new BankAccount();
var _lock = new object();
var tasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
tasks.Add(Task.Run(() =>
{
accountA.Balance -= 100;
accountB.Balance += 100;
}));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Account A remaining balance is {accountA.Balance}");
Console.WriteLine($"Account B remaining balance is {accountB.Balance}");
}
}
public class BankAccount
{
public decimal Balance { get; set; }
}
因為這段程式遇到 Data Raceing 同一時間有兩個不同的 Thread 對於資料競爭,所以執行的結果非預期的:
Account A remaining balance is 1400
Account B remaining balance is 9400
其實最簡單的方法就是加上 Lock 就可以處理:
for (var i = 0; i < 100; i++)
{
tasks.Add(Task.Run(() =>
{
lock (_lock)
{
accountA.Balance -= 100;
accountB.Balance += 100;
}
}));
}
可見性 (Visibility)
可見性指當在多個執行緒訪問同一個變數時,如果有其中一個執行緒修改此變數,其他執行緒也能夠得到此變數被修改。
搶門票範例
如果多個賣票櫃台賣票一張票後,沒有同時告知其他櫃台已經被賣掉一張,就可能發生超賣票裝況產生,如果在現實中發生就可能產生同一個位置突然出現兩個都買到此位置。
可見性注意事項
經常看到範例會介紹使用 volatile
或是 Volatile
來處理可見性,但官方文件有提到 multiprocessor system 使用時無法確保可見性,所以在實際使用時要注意。
On a multiprocessor system, a volatile read operation does not guarantee to obtain the latest value written to that memory location by any processor. Similarly, a volatile writeoperation does not guarantee that the value written would be immediately visible to other processors.
可見性範例
static async Task Main(string[] args)
{
var worker = new Worker();
Console.WriteLine("主執行緒:啟動工作執行緒...");
var workerTask = Task.Run(worker.DoWork);
// 等待 500 毫秒以確保工作執行緒已在執行
Thread.Sleep(500);
Console.WriteLine("主執行緒:請求終止工作執行緒...");
worker.RequestStop();
// 待待工作執行緒執行結束
await workerTask;
Console.WriteLine("主執行緒:工作執行緒已終止");
}
public class Worker
{
private bool _shouldStop;
public void DoWork()
{
bool work = false;
while (!_shouldStop)
{
work = !work;
}
Console.WriteLine("工作執行緒:正在終止...");
}
public void RequestStop()
{
_shouldStop = true;
}
}
滿多人會以為這個範例是因為有序性
造成執行結果不同,但其實並不是這樣可以參考此篇瞭解其實它背後的原因是可見性
。
// Debug
主執行緒:啟動工作執行緒...
主執行緒:請求終止工作執行緒...
工作執行緒:正在終止...
主執行緒:工作執行緒已終止
// Release
主執行緒:啟動工作執行緒...
主執行緒:請求終止工作執行緒...
有序性 (Ordering)
有序性是指程式是依照程式碼依序執行,但是有時候編譯器為了更有效率執行,有時候會改變程式碼的順序,所以常常在 Debug 模式(不會優化編譯)下沒有發生問題,在編譯 Release 上線後才發生錯誤。
販賣機範例
如果你有一台販賣機,你一定是希望裡面的效期快到商品先賣出去,所以你會優先放在最前面販賣,但有位好心的同事不小心把你的商品重新排列,讓販賣順序不是你所預期哪樣。
有序性範例
這時候又要出動 sharplab 來查看程式碼的順序,可以發現 debug
和 release
模式下的程式碼順序不同,所以在發布release
後可能會有問題的。
public class Example
{
public int x;
public void DoWork()
{
x = 5;
var y = x + 10;
Debug.WriteLine("x = " +x + ", y = " +y);
}
}
// Debug
public class Example
{
public int x;
public void DoWork()
{
x = 5;
int num = x + 10;
Debug.WriteLine(string.Concat("x = ", x.ToString(), ", y = ", num.ToString()));
}
}
// Release
public class Example
{
public int x;
public void DoWork()
{
x = 5;
int x2 = x;
}
}
參考資料
[C#.NET 拾遺補漏]10:理解 volatile 關鍵字