7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】Interlocked / Volatile を「正しく」使う最短ガイド

Posted at

この記事は、C# Advent Calendar 2025 16日目の記事です。

はじめに

マルチスレッドで共有変数を扱うと、たまに起きるやつ

  • カウンタが合わない
  • 「一度だけ実行」のはずが二度走る
  • 停止フラグを立てたのに、別スレッドが止まらない(っぽい)

こういう時にまず出番になるのがこの2つ

  • Interlocked:共有変数の更新を 原子的(途中で割り込めない) にする
  • Volatile:共有変数の読み書きを 他スレッドから"ちゃんと見える" ようにする(順序や最新性)

ここで大事なのは、2つは役割が違う ことです。

まず結論 - 使い分け

InterlockedVolatile は似た場面で登場しますが、役割は別物です。
Volatile は“見える”Interlocked は“壊れない”
まずは図と表で全体像だけ押さえます。

002.png

やりたいこと 使うもの
count++ みたいな 更新(Read→Modify→Write) を安全に Interlocked
停止フラグみたいに 「書く側」と「読む側」が分かれている 単純な共有 Volatile.Read/Write
「複数の値の整合性」を守りたい lock(Interlocked/Volatileだけで頑張らない)

Interlocked/Volatile は "単一の共有値" に強い道具です。複数の値をセットで守りたいなら lock が素直です。

ざっくりなイメージ

001.png

count=10 を両スレッドが read → それぞれ +1 → どちらも 11 を write してしまい、期待値12に届かない。

count++ は1命令じゃない

count++ は実質こうです。

  1. count を読む
  2. +1 する
  3. count に書く

複数スレッドが同時にやると、同じ値を読んで、同じ結果を書いてしまう(=増分が消える)ことがあります。

→ ここを 1回の"原子操作" にするのが Interlocked.Increment

② "見える" と "安全に更新できる" は別

  • Volatile は「書いた値が読めるようにする」寄り(可視性
  • Interlocked は「更新そのものを壊さない」寄り(原子性

Volatile を使っても count++ が安全にはならない、のはこのためです。

※Volatile.Read/Write は acquire/release(半フェンス)相当の制約を与えます。一方で、Interlocked 系の操作は一般により強いメモリバリア(フェンス)を伴うため、同期用途で使われます。

動作確認 - 検証用コンソールアプリ

実行手順

dotnet new console -n InterlockedVolatileDemo
cd InterlockedVolatileDemo
# Program.cs を下のコードに差し替え
dotnet run -c Release

Release 推奨(最適化が効いて、挙動の差が見えやすいです)

Program.cs

Volatile のデモは CPU/JIT の最適化に左右されるので「非Volatileが必ず止まらない」とは限りません。ただ「止まる/止まらないが環境依存になり得る」ことがポイントです。

Program.cs
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

public static class Program
{
    // Volatileデモ用:ローカルキャプチャではなくフィールドで明示
    private static int _stop;

    public static void Main()
    {
        Console.WriteLine($".NET: {Environment.Version}");
        Console.WriteLine($"CPU : {Environment.ProcessorCount} cores");
        Console.WriteLine();

        LostUpdateDemo();
        Console.WriteLine(new string('-', 60));

        StartOnceDemo();
        Console.WriteLine(new string('-', 60));

        VolatileStopFlagDemo();
        Console.WriteLine(new string('-', 60));

        Console.WriteLine("Done.");
    }

    // 1) count++ が壊れる(ロストアップデート)→ Interlocked で直す
    private static void LostUpdateDemo()
    {
        Console.WriteLine("[1] Lost update demo: count++ vs Interlocked.Increment");
        Console.WriteLine("    count++ は『読む→足す→書く』の3段階なので、同時実行で増分が消えることがある。");

        int workers = Math.Max(2, Environment.ProcessorCount);
        int itersPerWorker = 100_000;

        int expected = workers * itersPerWorker;

        // NG: count++
        {
            int count = 0;
            var tasks = new Task[workers];

            for (int w = 0; w < workers; w++)
            {
                tasks[w] = Task.Run(() =>
                {
                    for (int i = 0; i < itersPerWorker; i++)
                    {
                        count++; // 競合で増分が消える可能性
                    }
                });
            }

            Task.WaitAll(tasks);
            Console.WriteLine($"count++          expected={expected}, actual={count}");
        }

        // OK: Interlocked.Increment
        {
            int count = 0;
            var tasks = new Task[workers];

            for (int w = 0; w < workers; w++)
            {
                tasks[w] = Task.Run(() =>
                {
                    for (int i = 0; i < itersPerWorker; i++)
                    {
                        Interlocked.Increment(ref count); // 原子的に +1
                    }
                });
            }

            Task.WaitAll(tasks);
            Console.WriteLine($"Interlocked.Inc  expected={expected}, actual={count}");
        }
    }

    // 2) "一度だけ実行したい" → Exchange / CompareExchange の定番
    private static void StartOnceDemo()
    {
        Console.WriteLine("[2] Start-once demo: unsafe flag vs Interlocked.Exchange");
        Console.WriteLine("    if (flag == 0) { flag = 1; } は同時に通る可能性がある。");
        Console.WriteLine("    Exchange は『0→1にしたのが自分か?』を原子的に判定できる。");
        Console.WriteLine("    ※NG例は環境/タイミング次第で 1 に見えることもあります。");

        int workers = 64;

        // NG: 非原子的なフラグ操作
        {
            int started = 0;
            int startCalls = 0;
            var tasks = new Task[workers];

            for (int i = 0; i < workers; i++)
            {
                tasks[i] = Task.Run(() =>
                {
                    Thread.SpinWait(500);

                    if (started == 0)
                    {
                        started = 1;
                        Interlocked.Increment(ref startCalls);
                    }
                });
            }

            Task.WaitAll(tasks);
            Console.WriteLine($"unsafe flag      startCalls={startCalls}  (1 のはずが 2+ になることがある)");
        }

        // OK: Exchange で 0→1 を一人だけにする
        {
            int started = 0;
            int startCalls = 0;
            var tasks = new Task[workers];

            for (int i = 0; i < workers; i++)
            {
                tasks[i] = Task.Run(() =>
                {
                    Thread.SpinWait(500);

                    if (Interlocked.Exchange(ref started, 1) == 0)
                    {
                        Interlocked.Increment(ref startCalls);
                    }
                });
            }

            Task.WaitAll(tasks);
            Console.WriteLine($"Interlocked.Exch startCalls={startCalls}  (必ず 1)");
        }
    }

    // 3) 停止フラグ:Volatile.Read/Write で "見え方" を安定させる
    private static void VolatileStopFlagDemo()
    {
        Console.WriteLine("[3] Volatile demo: stop flag visibility");
        Console.WriteLine("    別スレッドの書き込みが『見える』保証を強めるのが Volatile。");
        Console.WriteLine("    ただし挙動はCPU/JITに依存するので、このデモは再現性が環境によって変わる。");

        // A) plain: 止まることも多いが、環境依存の不安定さが残る
        {
            _stop = 0;
            var sw = Stopwatch.StartNew();

            var t = Task.Run(() =>
            {
                while (_stop == 0) { Thread.SpinWait(1); }
            });

            Thread.Sleep(200);
            _stop = 1;

            bool finished = t.Wait(1000);
            Console.WriteLine($"plain read/write finished={finished}, elapsed={sw.ElapsedMilliseconds}ms");
        }

        // B) Volatile: 見え方を意図的にする
        {
            _stop = 0;
            var sw = Stopwatch.StartNew();

            var t = Task.Run(() =>
            {
                while (Volatile.Read(ref _stop) == 0) { Thread.SpinWait(1); }
            });

            Thread.Sleep(200);
            Volatile.Write(ref _stop, 1);

            bool finished = t.Wait(1000);
            Console.WriteLine($"Volatile R/W     finished={finished}, elapsed={sw.ElapsedMilliseconds}ms");
        }

        Console.WriteLine("    注意: Volatile は『更新を安全にする』道具ではない。count++ は Interlocked。");
    }
}

※このデモは「Volatile を使う意図」を示すもので、再現性そのものを保証するものではありません。

ここだけ覚えればOK(実務の早見表)

用途 コード
カウンタ Interlocked.Increment(ref count) / Add
一度だけ実行 Interlocked.Exchange(ref started, 1) == 0
(条件付きなら CompareExchange
停止フラグ Volatile.Read / Volatile.Write
複数値の整合性 lock

CompareExchange の補足

Exchange は無条件に書き換えますが、「今の値が○○のときだけ書き換えたい」なら CompareExchange です。

C#
int state = 0;

// state が 0 のときだけ 1 にしたい(成功したら 0 が返る)
if (Interlocked.CompareExchange(ref state, 1, 0) == 0)
{
    // 自分が遷移させた
}

volatile キーワード

volatile キーワードは、そのフィールドへのアクセスに対して、暗黙的に Acquire/Release のメモリバリアを挿入する 点で Volatile.Read/Write とほぼ同等ですが

  • 対応型が限定される(構造体などは不可)
  • 読み/書きのどちらが Volatile かコード上で分かりにくい
  • 明示的なバリアの粒度を選べない

などの理由から、近年の .NET では、Volatile.Read/Write を明示的に使った方が意図が明確 かなと思います。

まとめ

Interlocked は「更新を壊さない」、Volatile は「見え方を安定させる」。
迷ったら「単一の値なら Interlocked/Volatile、複数の整合性なら lock」で安全側に倒すのが手堅いです。

参考リンク

7
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?