導入
複数のタスクが同じデータを同時に書き換えると、更新がぶつかって値が壊れます(競合状態)。これを防ぐのがlock(一度に1つだけ通す)・Interlocked(軽量な原子操作)・並行コレクション(ConcurrentDictionary)です。並行処理を安全に書くための基本装備を押さえます。
図解
flowchart TB
T1["タスクA counter++"] --> G{"lock(1つずつ通す)"}
T2["タスクB counter++"] --> G
G --> C["counter を安全に更新"]
style G fill:#fff3e0
サンプル
// 100個のタスクが同じ counter を増やす。lock で「一度に1つだけ」を保証
int counter = 0;
object gate = new object();
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(Task.Run(() =>
{
lock (gate) // ここは一度に1タスクしか入れない
{
counter++;
}
}));
}
await Task.WhenAll(tasks);
Console.WriteLine(counter); // 100(lockのおかげで数え落とさない)
// Interlocked: 単純な増減なら lock より軽い原子操作
int atomic = 0;
Interlocked.Increment(ref atomic);
Console.WriteLine(atomic); // 1
// ConcurrentDictionary: 複数スレッドから安全に読み書きできる辞書
var dict = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
dict["a"] = 1;
Console.WriteLine(dict["a"]); // 1
- 複数タスクが同じ変数を書き換えると競合状態で値が壊れる
lock (何かのオブジェクト) { ... }で「一度に1つのタスクだけ」を保証する- 単純なカウントは
Interlocked.Increment/Addが軽い。共有辞書はConcurrentDictionary - ロックの掛けすぎは遅くなる・デッドロックの元。共有をなるべく減らす設計が最善
演習
int value = 0;
// TODO: Interlocked.Increment を使って value を 1 増やしてください
___
Console.WriteLine(value);
- 期待される出力:
1
ヒント1を見る
Interlocked.Increment(ref value); はvalueを原子的に+1します
ヒント2を見る
refを付けてInterlocked.Increment(ref value);
まとめ
- 共有データの同時更新は競合状態を招く
lockで排他、単純な数え上げはInterlocked、共有辞書はConcurrentDictionary- そもそも共有を減らす設計がいちばん安全
次回: 非同期に1件ずつ届くデータを扱うIAsyncEnumerableです。