SemaphoreSlim を使って並列実行を制御する

たまたま、SemaphoreSlim を使った並列実行の制御をするコードを読んだので、ちゃんと理解するために、サンプルコードを書いて調べてみたので、ブログに書いておく。

# Semaphore と SemaphoreSlim

Semaphore という概念はこちらのブログがとってもわかりやすい。

* [セマフォ (semaphore)](https://wa3.i-3-i.info/word13357.html)

つまるところ、並列実行をするときに、並列実行数を制御できる仕組みだ。数が付いたロックのようなもの。どのような時に使うかというと、並列実行をして処理速度を上げたいんだけど、あまりたくさんの数で並列実行させてしまうと、受け手がパンクする、、、とかいうユースケースが考えられる。

この概念を表す Semaphore と SemaphoreSlim というクラスが存在する。Semaphore の方は、このセマフォの仕組みがWin32セマフォオブジェクトのラッパである。SemaphoreSlim シンプルな実装の方で、ローカル（つまり自分のアプリ内）のみのセマフォをサポートし、Semaphore の方はシステムワイドなNamed Semaphore という概念を実装しているので、システムワイドにセマフォを共有してそれに名前を付けたりできるようだ。.NET でプログラミングするなら大抵の人は SemaphoreSlim で事が済む気がする。詳しくはこちら。

* [Semaphore and SemaphoreSlim](https://docs.microsoft.com/en-us/dotnet/standard/threading/semaphore-and-semaphoreslim)

ちなみに[非同期：awaitを含むコードをロックするには？（SemaphoreSlim編）［C#、VB］](http://www.atmarkit.co.jp/ait/articles/1411/11/news117.html)こちらの記事では、async メソッドを使いながらロックを書けるのに使うといわれていたが、個人的には目的が違う気がする。ただ確かにセマフォを１に設定すると、実質ロックをかけているのとかわらないので興味深い。


今回は SemaphoreSlim のオフィシャルページにのっていたサンプルがわかりやすかったのでちょっとだけ変えて実装してみた。

* [SemaphoreSlim Class ](https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=netframework-4.7.2) 

リファレンス実装はこちらから見れる

* [referencesource/mscorlib/system/threading/SemaphoreSlim.cs
](https://github.com/Microsoft/referencesource/blob/90b323fe52bec428fe4bd5f007e9ead6b265d553/mscorlib/system/threading/SemaphoreSlim.cs#L87)

ちなみに、リファレンス実装にでてくる`Contract` クラスはアサーション。契約による設計というやつでしょうか。

* [コード コントラクト](https://docs.microsoft.com/ja-jp/dotnet/framework/debug-trace-profile/code-contracts)

# SemaphoreSlim の使い方

とてもシンプルで、

```csharp
var semaphore = new SemaphoreSlim(0, 3);
```

セマフォを生成する。第一引数は、初期値で、第二引数は最大値。つまり最初のセマフォは０なので、一切新しいタスクを実行できないが、増やしたら３まで並列実行できるようになる。そして、並列実行させている箇所で次のように書く。

```csharp
await semaphore.WaitAsync();  // もしくは、semaphore.Wait();
  :
semaphore.Release()
```

`Wait/WaitAsync()` の箇所でセマフォが待ち状態に入る。上記のリファレンス実装を見ると内部で `lock` を使って自分で管理して実装していた。セマフォが使える状態になったら、以降の処理を実行する。`Release()` でそのセマフォをリリースする。つまり次の並列実行している関数などがセマフォのWait で待っているものが、処理をスタートできるようになる。

単純にこれだけ。じゃあ実際にコードを書いてみてみよう。

# サンプル

```csharp
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SemaphoreSlimSpike
{
    class Program
    {
        private static SemaphoreSlim semaphore;
        private static int count;
        static void Main(string[] args)
        {
            semaphore = new SemaphoreSlim(0, 3);
            Console.WriteLine($"{semaphore.CurrentCount} taks can enter the semaphore.");

            Task[] tasks = new Task[5];

            foreach(var i in Enumerable.Range(0, 5))
            {

                //// Async
                //tasks[i] = Task.Run(async () =>
                //{
                //    Console.WriteLine($"Task {Task.CurrentId}: {i} begins and wait for the semaphore. {DateTime.Now.ToString()}");
                //    await semaphore.WaitAsync();
                //    Interlocked.Add(ref count, 1);
                //    Console.WriteLine($"Task {Task.CurrentId}: {i} enters the semaphore. {DateTime.Now.ToString()}");
                //    await Task.Delay(TimeSpan.FromSeconds(3 + count)); // It will change the end time for the concurrently started thread.
                //    Console.WriteLine($"Task {Task.CurrentId}: {i} releases the semaphore; previous count {semaphore.Release()}");
                //    Console.WriteLine($"Task {Task.CurrentId}: {i} ends the semaphore. {DateTime.Now.ToString()}");

                //});
                
                // Sync
                tasks[i] = Task.Run(() =>
                {
                    Console.WriteLine($"Task {Task.CurrentId}: {i} begins and wait for the semaphore. {DateTime.Now.ToString()}");
                    semaphore.Wait();
                    Interlocked.Add(ref count, 1);
                    Console.WriteLine($"Task {Task.CurrentId}: {i} enters the semaphore. {DateTime.Now.ToString()}");
                    Thread.Sleep((3 + count) * 1000); // It will change the end time for the concurrently started thread.
                    Console.WriteLine($"Task {Task.CurrentId}: {i} releases the semaphore; previous count {semaphore.Release()}");
                    Console.WriteLine($"Task {Task.CurrentId}: {i} ends the semaphore. {DateTime.Now.ToString()}");

                });
            }

            // Wait for half a second to allow all the tasks to start and block .
            Thread.Sleep(500);

            Console.WriteLine("Main thread call Release(3)");
            // increase avairable Semaphore
            semaphore.Release(3);
            Console.WriteLine($"{semaphore.CurrentCount} tasks can enter the semaphore.");
            Task.WaitAll(tasks);
            Console.WriteLine("Main thread exits.");
            Console.ReadLine();

        }
    }
}

```

これがどんな処理をしているかというと、最初はセマフォが使えない（初期値０なので）Task.Run の中でマルチスレッドもしくは、並列で async メソッドを実行する。そして、`semaphore.Wait()/WaitAsync()` でストップする。セマフォで使えるのがゼロなので。

```csharp
semapore.Release(3);
```

の箇所に来るとセマフォが３つリリースされる。つまり、セマフォが使える最大値になる。そしたら、セマフォでブロックされていた３つぶんのスレッドもしくは、async メソッドが動き出す。５つのTaskがあるが同時に動くのは３つまで。それらが終わるのをまって、次のスレッドが動き出すという挙動になる。

## interlock 

途中で Interlocked というクラスのメソッドが出てくる。これは、ロックをかけるのだが、`lock` よりもずっと負荷を小さくできるやり方で、中で参照されている変数にロックを書けることができる。

```charp
Interlocked.Add(ref count, 1);
```

* [第3回　マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御（前編） ― (3/3)](http://www.atmarkit.co.jp/ait/articles/0505/25/news113_3.html)

実際にロックの方式を比較されている方がいます。

* [マルチスレッドで高速なC#を書くためのロック戦略](https://qiita.com/tadokoro/items/28b3623a5ec58517d431)


## 実行結果 (sync)

```
0 taks can enter the semaphore.
Task 5: 4 begins and wait for the semaphore. 02/08/2018 16:27:54
Task 1: 0 begins and wait for the semaphore. 02/08/2018 16:27:54
Task 4: 3 begins and wait for the semaphore. 02/08/2018 16:27:54
Task 2: 1 begins and wait for the semaphore. 02/08/2018 16:27:54
Task 3: 2 begins and wait for the semaphore. 02/08/2018 16:27:54
Main thread call Release(3)
3 tasks can enter the semaphore.

Task 1: 0 enters the semaphore. 02/08/2018 16:27:54
Task 5: 4 enters the semaphore. 02/08/2018 16:27:54
Task 4: 3 enters the semaphore. 02/08/2018 16:27:54

Task 2: 1 enters the semaphore. 02/08/2018 16:28:00
Task 1: 0 releases the semaphore; previous count 0
Task 5: 4 releases the semaphore; previous count 0
Task 5: 4 ends the semaphore. 02/08/2018 16:28:00

Task 3: 2 enters the semaphore. 02/08/2018 16:28:00
Task 1: 0 ends the semaphore. 02/08/2018 16:28:00
Task 4: 3 releases the semaphore; previous count 0
Task 4: 3 ends the semaphore. 02/08/2018 16:28:00
```

思った通りの動きをしている。ちなみに、async バージョンを書いてみると、同じように動くが、`Task.CurrentId` が`null`になるのはなんでだろう？

## 実行結果 (async)

実行結果は同じだが、Task.CurrentId が最初の beginsのところ以降、つまり、Semaphore の有効範囲内では null になっているので表示されない。

```
0 taks can enter the semaphore.
Task 9: 4 begins and wait for the semaphore. 02/08/2018 16:37:50
Task 3: 1 begins and wait for the semaphore. 02/08/2018 16:37:50
Task 1: 0 begins and wait for the semaphore. 02/08/2018 16:37:50
Task 5: 2 begins and wait for the semaphore. 02/08/2018 16:37:50
Task 7: 3 begins and wait for the semaphore. 02/08/2018 16:37:50
Main thread call Release(3)
0 tasks can enter the semaphore.
Task : 0 enters the semaphore. 02/08/2018 16:37:50
Task : 1 enters the semaphore. 02/08/2018 16:37:50
Task : 4 enters the semaphore. 02/08/2018 16:37:50
Task : 2 enters the semaphore. 02/08/2018 16:37:56
Task : 1 releases the semaphore; previous count 0
Task : 4 releases the semaphore; previous count 0
Task : 4 ends the semaphore. 02/08/2018 16:37:56
Task : 3 enters the semaphore. 02/08/2018 16:37:56
Task : 1 ends the semaphore. 02/08/2018 16:37:56
Task : 0 releases the semaphore; previous count 0
Task : 0 ends the semaphore. 02/08/2018 16:37:56
```

# リソース

他に参考になりそうなリソース


かずき師匠のエントリ。理解するために await クラスを自作するという素晴らしいセンス！

* [await可能なクラスをつくってみよう](https://blog.okazuki.jp/entry/2013/07/21/225624)

公式のスレッド関係の一覧が載っている

* [Threading Objects and Features](https://docs.microsoft.com/en-us/dotnet/standard/threading/threading-objects-and-features)

今回のサンプル

* [TsuyoshiUshio/SemaphoreSlimSpike](https://github.com/TsuyoshiUshio/SemaphoreSlimSpike)