並行程式設計核心挑戰在於共享資料競爭。本文以銀行帳戶餘額並行增減為例,剖析競爭成因並提出四大解決方向。
10 個 Task 各自將 Balance 遞增 1000 次,預期結果 10000,實際每次執行結果皆不同且小於 10000:
class BankAccount { public int Balance { get; set; } }
var account = new BankAccount();
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++) {
tasks[i] = Task.Factory.StartNew(() => {
for (int j = 0; j < 1000; j++) account.Balance++;
});
}
Task.WaitAll(tasks);
Console.WriteLine($"Expected: 10000, Actual: {account.Balance}");
account.Balance++ 非原子操作,包含三步驟:
兩個 Task 交錯執行時:
Task1 讀取 0 → Task2 讀取 0 → Task1 寫入 1 → Task2 寫入 1
// 兩次遞增,結果卻只增加 1
因作業系統排程不確定性,每次執行交錯順序不同,導致結果隨機。
| 策略 | 核心思想 | 比喻(分蛋糕) |
|---|---|---|
| 1. 循序執行 | 一次只允許一個 Task 存取共享資料 | 一次只讓一個人拿蛋糕 |
| 2. 資料不可變 | 共享資料唯讀,不可修改 | 蛋糕只能看不能吃 |
| 3. 隔離 | 每個 Task 擁有資料私有複本 | 每人分一塊自己的蛋糕 |
| 4. 同步 | 協調 Task 存取順序(鎖、訊號等) | 排隊依序拿蛋糕 |
本質退化為單執行緒,放棄平行優勢,僅適合極簡單場景。
C# 關鍵字 const(編譯期常數,型別存取)與 readonly(執行期唯讀,建構函式可指派):
public class ImmutableBankAccount {
public const int AccountNumber = 123456;
public readonly int Balance;
public ImmutableBankAccount(int initial) { Balance = initial; }
}
適用於真正不可變的資料結構,限制較大,非通用方案。
ThreadLocal<T>、區域狀態、資料分割lock、Monitor、Interlocked、Semaphore、ReaderWriterLockSlim 等