C#のインクリメント演算子(++)がスレッドセーフじゃないから色々調べた話。

17年プログラマをやっていて、いまさら `i++` がスレッドセーフじゃないことを知った [^1] ことを反省しての記事です。
[^1]: 今までそんな実装の需要がなかったんだもの。。。

# ざっくりいうと

* C#のインクリメント演算子はスレッドセーフではありません。
* スレッドセーフを求める場合は、`System.Threading.Interlocked.Increment`を利用しましょう。
* `System.Threading.Interlocked.Increment`がスレッドセーフたる理由を調べてみた。

# 本文

## 検証してみる

以下のMSTestを実行してみたところ、テストはエラーになりました。

```c#
        [TestMethod]
        public void TestIncrementersThreadSafe()
        {
            var tasks = new List<Task>();
            int counter = 0;
            for (int i = 0; i < 250; i++)
            {
                tasks.Add(Task.Run(() => counter++));
            }
            Task.WaitAll(tasks.ToArray());
            Assert.AreEqual(250, counter);
        }
```

結果
`Assert.AreEqual に失敗しました。<250> が必要ですが、<247> が指定されました。`

次のように修正したところ、テストは成功しました。

```c#
        [TestMethod]
        public void TestIncrementersThreadSafe()
        {
            var tasks = new List<Task>();
            int counter = 0;
            for (int i = 0; i < 250; i++)
            {
                tasks.Add(Task.Run(() => Interlocked.Increment(ref counter)));
            }
            Task.WaitAll(tasks.ToArray());
            Assert.AreEqual(250, counter);
        }
```

## どういう仕組みなの？

この時点で、なにかしらLockを使ってるんだろうなぁと考えたのですが、どのポインタに対してLockを取っているのかが気になりました。それを知っておかないと、安全には使えませんよね。

というわけで、ソースを覗いてみました。
[Microsoft Reference Source - Interlocked.cs](https://referencesource.microsoft.com/#mscorlib/system/threading/interlocked.cs)

```c#
    public static class Interlocked
    {
        //...(中略)...
        [ResourceExposure(ResourceScope.None)]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        public static int Increment(ref int location)
        {
            return Add(ref location, 1);
        }
        //...(中略)...
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        public static int Add(ref int location1, int value) 
        {
            return ExchangeAdd(ref location1, value) + value;
        }
        //...(中略)...
        [ResourceExposure(ResourceScope.None)]
        [MethodImplAttribute(MethodImplOptions.InternalCall)]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        internal static extern int ExchangeAdd(ref int location1, int value);
```

ソースを見ても、Lockをかけているような実装は見当たりません。
どうやらカギは、外部メソッド `ExchangeAdd` にあるようです。

でも、うーん、それ以上のヒントは見当たりません。

.NETのドキュメントを読んでも、次のように書かれているだけ。
[Microsoft Docs - Interlocked Class](https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked?view=netframework-4.8)
`The Add method atomically adds an integer value to an integer variable and returns the new value of the variable.`
↓Google翻訳
`Addメソッドは、整数値を整数変数にアトミックに追加し、変数の新しい値を返します。`

「アトミックに」できている理由を知りたいんだよこっちは。。。

## ヒントはstackoverflowで見つけた

こんなやり取りを見つけました。
[How does Interlocked work and why is it faster than lock? [duplicate]](https://stackoverflow.com/questions/18640327/how-does-interlocked-work-and-why-is-it-faster-than-lock)
↓
[Reed Copsey](https://stackoverflow.com/users/65358/reed-copsey)さんの回答によると:
`Interlocked has support at the CPU level, which can do the atomic operation directly.`
↓Google翻訳 注：カッコ内は私の意訳
`InterlockedはCPUレベルでサポートされており、(これを利用することで)アトミック操作を直接実行できます。`

CPUレベルで！？

続けて、こうおっしゃっています。
`For example, Interlocked.Increment is effectively an XADD`
↓Google翻訳
`たとえば、Interlocked.Incrementは事実上XADDです`

なるほど、XADDね！(・∀・)

いや、なんですか、XADDて。。。(;´･ω･)

## XADDを調べた

探すのが難しかったのですが、私の結論は以下の記事で行きつきました。

[atomic fetch-and-add vs compare-and-swap - An Oracle blog about Transactional locks - Dave's Blog](https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap)
↓
`The x86 architecture exposes LOCK:XADD which I'll use in the discussion below. `
↓Google翻訳
`x86アーキテクチャは、以下の説明で使用するLOCK：XADDを公開します。`

そしてWikipediaの以下のあたりの記述。
[Fetch-and-add - wikipedia.org](https://en.wikipedia.org/wiki/Fetch-and-add#x86_implementation)

つまり、x86アーキテクチャは、加算をアトミックに(つまり一連の操作として)行うことをサポートしていると。

なるほど、`Interlocked.Increment`はこれを利用しているから、加算している間に他スレッドの割り込みを受けることがなく、スレッドセーフになっていると。納得！

## 性能ってどうなの？

まとめてくださっている記事がありました。
[マルチスレッドで高速なC#を書くためのロック戦略 - @tadokoro](https://qiita.com/tadokoro/items/28b3623a5ec58517d431)

スレッドセーフの中では、`Interlocked.Increment`が最速ですね。さすがCPUレベル。

# 最後に

私の中のエンジニアが「それを知っておかないと、安全には使えませんよね。」とか言い出したために、思ってたよりも~~めんどくさ~~大変な調査になりましたが、いろいろ有意義な情報にたどり着くことが出来ました。

但し、`CPUレベル`な部分について殆ど知識がない中、公開されている情報や回答をつなぎ合わせて推論し出した結論ですので、理解が間違っている部分があるかもしれません。その場合はご教授いただけると有り難いです。

## ところで `x64アーキテクチャ`の場合はどうなの？

うん、力尽きた。( ;∀;)
どなたかご教授いただけましたら、ありがたいです。。。（情報のソースもあると嬉しいです）