マルチスレッド環境で数値を更新する際、単純な加算演算子(+= や ++)を使用すると、データの不整合が発生するリスクがあります。これは、加算処理が「読み取り、計算、書き込み」という3つのステップに分かれているため、スレッド間で割り込みが発生するためです。
lock 文を使用すれば解決できますが、単純な数値の更新に対してはオーバーヘッド(処理コスト)が大きすぎます。 このような場合、CPUレベルのアトミック(不可分)な操作を提供する System.Threading.Interlocked クラスを使用することで、高速かつ安全に数値を更新できます。
以下に、複数の集計スレッドから共有のカウンターを安全に更新する実装例を解説します。
Interlocked.Addによる安全な加算処理
このコードは、Webサーバーのアクセスログ集計をシミュレーションしています。複数のスレッドが同時に「アクセス数」を報告し、共有された変数の値を更新します。Interlocked.Add を使用することで、ロックを掛けずに正確な集計を実現します。
サンプルコード
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
public static void Main()
{
// 1. 集計用インスタンスの生成
var counter = new AccessCounter();
// 処理の並列数(シミュレーションする同時接続数)
int taskCount = 5;
Console.WriteLine($"計測を開始します(並列タスク数: {taskCount})...");
// 2. 複数のタスクを生成して並列実行
// 各タスクが独立してカウンターを更新しに行きます
var tasks = Enumerable.Range(0, taskCount)
.Select(_ => Task.Run(() => counter.SimulateTraffic()))
.ToArray();
// 全てのタスクが終了するのを待機
Task.WaitAll(tasks);
// 3. 結果の検証
Console.WriteLine("\n--- 結果発表 ---");
Console.WriteLine($"期待される総アクセス数: {taskCount * AccessCounter.BatchSize * AccessCounter.HitsPerBatch}");
Console.WriteLine($"実際の総アクセス数 : {AccessCounter.TotalHits}");
}
}
public class AccessCounter
{
// スレッド間で共有される静的フィールド
// lockなしで安全に更新するためには Interlocked クラスを利用する
public static int TotalHits = 0;
// シミュレーション設定
public const int BatchSize = 100; // 1タスクあたりのループ回数
public const int HitsPerBatch = 10; // 1回の更新で加算する数
public void SimulateTraffic()
{
for (int i = 0; i < BatchSize; i++)
{
// 擬似的な処理時間
Thread.Sleep(5);
// 共有リソースへの加算処理
RecordHits(HitsPerBatch);
}
}
/// <summary>
/// カウンターへの安全な加算処理
/// </summary>
private void RecordHits(int count)
{
// Interlocked.Add(ref 変数, 加算値)
// メモリ上の TotalHits に対して、他のスレッドに邪魔されることなく
// 「読み取り・加算・書き込み」を一息に行います。
// 通常の TotalHits += count; では競合が発生し、値がズレる可能性があります。
Interlocked.Add(ref TotalHits, count);
// 1だけ増やす場合は Increment も利用可能
// Interlocked.Increment(ref TotalHits);
}
}
解説と技術的なポイント
1. 「+=」演算子の危険性
TotalHits += 10; というコードは、CPUにとっては以下の3つの命令に分解されます。
- メモリから
TotalHitsの値をレジスタに読み込む - レジスタの値に
10を足す - 計算結果をメモリの
TotalHitsに書き戻す
マルチスレッド環境では、スレッドAが書き戻す前に、スレッドBが古い値を読み込んでしまうことがあり、結果として「スレッドAの加算分が上書きされて消える」という現象が起こります。
2. アトミック操作 (Atomic Operation)
Interlocked クラスのメソッド(Add, Increment, Exchange など)は、上記の3ステップを「アトミック(不可分)」な操作として実行します。ハードウェアレベルでメモリバスをロックするなどの制御が行われ、操作が完了するまで他のスレッドがそのメモリにアクセスできないことが保証されます。
3. lock文との使い分け
- Interlocked: 単純な整数値のインクリメント、加算、入れ替えに特化しています。非常に高速です。
- lock: 複数の行にわたる処理ブロック全体を保護したい場合や、複雑なオブジェクトの整合性を保つ場合に使用します。単純な計算に使うにはコストが高すぎます。
4. ref キーワード
Interlocked のメソッドは、変数の「値」ではなく「メモリ上の場所(参照)」を操作する必要があるため、引数には必ず ref キーワードを付けて渡します。これにより、メソッド内部で変数の値を直接書き換えることが可能になります。
