再入門C#：非同期処理・ロック

普段Asyncのプログラムは書いていて、非同期処理のコンセプトもふわっとしっているので、いざとなれば調べたらトラブルシュートもできるのだが、プログラマとしてレベルを上げるために、複雑な非同期処理を何も見ることなくがっつりコードが書けるようになりたいと思い立ち、基礎の基礎からコードを書いてみることにしました。基本的にいつもお世話になっている岩永先生のサイトや、他で学んだ内容を自分なりに理解しながら、整理するだけですので、[本家のサイト](https://ufcpp.net/study/csharp/sp_thread.html)のほうがいろいろ有用と思います。

一つだけこのシリーズで気を付けたいのは、「コンファタブル」なレベルをキープすることです。例えば、私は最初にコンカレントキューから始めようと思ったのですが、通常のキューとの、Enqueueの挙動の違いが気になってコードを読んで、volatile というキーワードが出てきたり、他のライブラリが出てきたりして一向に終わりませんでした。今の自分は達人ではないので、達人のように一気になろうとするのは問題です。だから、自分が楽に理解できるレベルをキープしながら一歩一歩進めたら、きっと、コンカレントライブラリの実装も全部読めるようになるでしょう。でもそれまでは、自分が難しいと思ったらそこには何か問題があるはずので、自分が楽に理解できるレベルをキープすることを念頭においてやってみたいと思います。

今回は

* [マルチスレッド](https://ufcpp.net/study/csharp/sp_thread.html)
の内容を学びます。

# マルチスレッドが必要な理由

マルチスレッドはコンピューティングの様々な場面で必要です。代表的なものがGUIのプログラミングです。何か時間がかかる処理、例えばダウンロードを行っている間に、GUIが一切反応しなくなってしまったら相当利便性が低くなります。そういった時に、コンピュータが並列で処理ができると、ダウンロードを行っている間に、キーボードからの入力を受け付けて、さらに、バックグラウンドで別のクリーンアップ処理を走らせておくといったことが可能になります。特に実行時間が長くかかるような処理は、平行で処理をして、終了を待っている間、別の処理やイベントを済ませておく方が良いでしょう。そういったときに使えるテクニックです。

# スレッドとは

スレッドは１連の処理の流れのことです。プログラムが上から下まで実行されるときのイメージです。マルチスレッドはそれが複数同時に進行するイメージになります。

# スレッドプールとは

スレッドプールとは、スレッドが必要なたびに新しいスレッドを作っていると、リソースが枯渇してしまったり、効率が悪かったりします。そこで、スレッドをプールする仕組みがあると良いのですが、これを自分でコードを書くのはめんどくさいでしょう。C#ではこの辺をよしなにやってくれるTaskというクラスがありますが、これはまた次の機会に。

# レースコンディション

マルチスレッドで問題になるのが、[レースコンディション](https://ja.wikipedia.org/wiki/%E7%AB%B6%E5%90%88%E7%8A%B6%E6%85%8B)という問題です。プログラムの処理の結果が予期しない状態になるのｄせうが、その原因が、プログラミングのパーツが依存関係をもっていて、イベントや処理実行の順番の違いなどで発生します。あまり深く考えずにマルチスレッドで、ステートのあるコードを書くと簡単します。

# レースコンディションが発生する例

下記の例では [Parallel.For](https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-write-a-simple-parallel-for-loop)は、マルチスレッドで指定した数で平行にActionの処理を実行してくれます。次の例では、0 -> 19 まで、並列で、アクションを実行してくれます。ちなみに、並列数を制御したいとか、CancellationToken を使いたいとかいう場合は、[ParallelOptions](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.paralleloptions?view=netframework-4.8)というオプションを使えるようになるオーバーロードがあります。また戻り値のオブジェクトでは、実行が正しく実行されたかのフラグを保持しています。

さて、この場合は `i => { }` のアクションの部分が並列で実行されますが、平行で、numを更新にいっています。このnumが同時に更新されるために、本来実行結果では、20 * 20 の 400 の数だけカウントされるはずが、平行実行でそれぞれが更新する結果、実行結果が毎回変わります。


```csharp
        static  void MultiThread()
        {
            const int ThreadNum = 20;
            const int LoopNum = 20;
            int num = 0;

            Parallel.For(0, ThreadNum, i =>
            {
                for (int j = 0; j < LoopNum; j++)
                {
                    int tmp = num;
                    Thread.Sleep(1);
                    num = tmp + 1; 
                }
            });
            Console.Write($"{num} ({ThreadNum * LoopNum})");
        }
```

結果

```
80 (400)
```


# ロックを使用する

解決の方法としては、ここは、平行で実行すると困るという箇所を、クリティカルセクションと呼ぶのですが、そのクリティカルセクションに対して、ロックをかけます。基本的な書き方は、ロックオブジェクトというオブジェクトをつくります。ここでは`syncObj`がそれに該当します。
そして、クリティカルセクションを`lock(syncObj)`で囲います。すると、syncObjにロックがかかって、１つのスレッドしか同時にロックができなくなります。つまり同時に１つのスレッドしかこのセクションを実行できません。１つのスレッドがセクションの実行を終えると、ロックが解放されて、他のスレッドが使用可能になります。

```csharp
        static void MultiThreadWithLock()
        {
            const int ThreadNum = 20;
            const int LoopNum = 20;
            int num = 0;
            var syncObj = new object(); 
            Parallel.For(0, ThreadNum, i =>
            {
                for (int j = 0; j < LoopNum; j++)
                {
                    lock (syncObj)
                    {
                        int tmp = num;
                        Thread.Sleep(1);
                        num = tmp + 1;
                    }
                }
            });
            Write($"{num} ({ThreadNum * LoopNum})");
        }
```

実行結果は正しくなります。

```
400 (400)
```

# 実行時間

ロックをかけると、当然実行時間にインパクトが出ます。ベンチマークをとってみました。あたりまえですが、ロックが無いのが最速です（答え間違っていますが）ロックをすると極端に実行時間が増えます。ちなみに２個目のMontorLockは、`Monitor.Enter()/Minotor.Exec()`を使ったロックで、元のブログに載っていたサンプルを実行した結果です。lockは、それのシンタックスシュガーですので、似たような実行結果になります。


```
80 (400) True The first(no lock): Elapsed Time: 1442 ms
400 (400)The second(Monitor lock): Elapsed Time: 6189 ms
400 (400)The third(lock): Elapsed Time: 6231 ms
```