【C#】Parallelでマルチスレッド処理の注意点を学ぶ

# TL;DR
1. C#はParallelで簡単に並列処理を実装できる
1. マルチスレッド処理なのでConcurrentやlockを適切に使用しないと事故る

# はじめに
C#ではParallelで簡単に並列処理を実装できてパフォーマンスの改善が望めます。
しかしなまじマルチスレッド処理なので、しっかり理解していないと事故ります。

# 問題のあるコード
名前を10個並べたListから、indexをkeyとした連想配列を作るコードです。
ぱっと見、ループ部分を並列で実行しているだけで問題ないように見えます。

```csharp
public void TestParallelDictionary()
{
    var names = new List<string>()
    {
        "John Doe",
        "Jane Smith",
        "Alice Johnson",
        "Bob Brown",
        "Charlie Davis",
        "Dana White",
        "Eve Black",
        "Frank Green",
        "Grace Miller",
        "Henry Wilson"
    };

    var dic = new Dictionary<long, string>();

    Parallel.ForEach(names, (name, state, index) =>
    {
        dic.Add(index, name);
    });

    _helper.WriteLine(JsonSerialize(dic));
}
```

```text
{
  "0": null,
  "0": null,
  "0": "John Doe",
  "8": "Grace Miller",
  "5": "Dana White",
  "1": "Jane Smith",
  "9": "Henry Wilson",
  "3": "Bob Brown",
  "2": "Alice Johnson",
  "6": "Eve Black"
}
```

**例外にならず**nullが入ってきました。
また実行回数を増やすと**たまに**例外を吐きます。

```tex
One or more errors occurred. (An item with the same key has already been added. Key: 0)
```

# Dictionaryはスレッドセーフではない
Dictionary含め、普段常用するような配列はスレッドセーフではありません。

https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.generic.dictionary-2?view=net-8.0#thread-safety

ですので配列の同じ要素を使おうとして整合性を保てなくなりnullが入っていると思われます。

Listを使用しても同様にスレッドセーフではないのでnullや例外が発生します。

```csharp
[Fact]
public void TestParallelList()
{
    var names = new List<string>()
    {
        "John Doe",
        "Jane Smith",
        "Alice Johnson",
        "Bob Brown",
        "Charlie Davis",
        "Dana White",
        "Eve Black",
        "Frank Green",
        "Grace Miller",
        "Henry Wilson"
    };

    var list = new List<string>();

    // このくらい試行回数を増やさないと発生しない
    for (int i = 0; i < 1000; i++)
    {
        Parallel.ForEach(names, (name, state, index) =>
        {
            list.Add(name);
        });
    }

    foreach (var s in list)
    {
        Assert.NotNull(s);
    }
}
```

結果
```
Assert.NotNull() Failure: Value is null
```

# 解決策① Concurrentな配列を使用する
C#ではスレッドセーフな配列が用意されています。

https://learn.microsoft.com/ja-jp/dotnet/standard/collections/thread-safe/

ConcurrentDictionaryはスレッドセーフなので、配列の安全性が確保されます。

```csharp
public void TestParallelConcurrentDictionary()
{
    var names = new List<string>()
    {
        "John Doe",
        "Jane Smith",
        "Alice Johnson",
        "Bob Brown",
        "Charlie Davis",
        "Dana White",
        "Eve Black",
        "Frank Green",
        "Grace Miller",
        "Henry Wilson"
    };

    var dic = new ConcurrentDictionary<long, string>();

    Parallel.ForEach(names, (name, state, index) =>
    {
        dic.TryAdd(index, name);
    });

    _helper.WriteLine(JsonSerialize(dic));
}
```

当たり前ですが基本的にConcurrentじゃないクラスを使用したほうが速いので、なんでもかんでもこれで実装するのはやめましょう。

https://learn.microsoft.com/ja-jp/dotnet/standard/collections/thread-safe/when-to-use-a-thread-safe-collection#concurrentdictionary-vs-dictionary

 > キーまたは値の読み取りのみを行う場合、ディクショナリがスレッドによって変更されないのであれば同期は不要なため、Dictionary<TKey,TValue> の方が高速です。

# 解決策② lockをかけてしまう
基本的にConcurrentで大丈夫だと思いますし簡単ですが、並列処理内でごちゃごちゃやりたいときは対象のオブジェクトをlockしてしまうのも手かと思います。

```csharp
public void TestParallelLockDictionary()
{
    var names = new List<string>()
    {
        "John Doe",
        "Jane Smith",
        "Alice Johnson",
        "Bob Brown",
        "Charlie Davis",
        "Dana White",
        "Eve Black",
        "Frank Green",
        "Grace Miller",
        "Henry Wilson"
    };

    var dic = new Dictionary<long, string>();

    Parallel.ForEach(names, (name, state, index) =>
    {
        lock (dic)
        {
            dic.Add(index, name);
        }
    });
    
    _helper.WriteLine(JsonSerialize(dic));
}
```

# まとめ
Parallelは簡単にパフォーマンスを改善できる手段ですが、並列処理を理解していないと気づきづらい事故が発生します。
適切に使用していきましょう。