[.NET] Monitor.Wait/PulseよりもManualResetEventSlimを使う

# 前置き
[検証:Task + CancellationTokenSourceでは.NET4.0以前のスレッドの実行をブロックするメソッドを中断できない](http://qiita.com/laughter/items/75d943bb6d73a9796e58)という投稿をしたが、その中で、

> (`CancellationTokenSource`で)中断できないメソッドの中には、`Thread.Interrupt`で中断できたメソッド(`Monitor.Wait`)も含まれる。
>そのため、`Monitor.Wait`を使用したい場合、`Thread`を使用する意味が依然として存在するように思える。

という記述をした。

しかしながら、事実として.NET4.0以降の環境では`Thread`よりも`Task`の利用が推奨されている。
では、`Task`でどのようにスレッドをブロックする処理を実現すればよいのか。中断できないことを知りつつも、`Monitor.Wait`を使用するしかないのだろうか。
それとも、代わりのクラスが用意されているのだろうか。
あるいは、小刻みに`Monitor.Wait`と`CancellationToken.ThrowIfCancellationRequested`を繰り返すクラスを自分で実装するべきなのだろうか。

いろいろ調べてみたが、C#や.NETをあつかっている情報のなかには、答えらしきものがみあたらなかった。
そこで、視野を広げて、Javaの情報にあたってみた。
そのため今回の投稿は独自研究の色が強く、C#に詳しい諸氏にとってつっこみどころがある内容になっているはずである。
しかし、間違ったところがあったとしても、それが正されることで、私や同程度の知識のプログラマにとって有益な結果が得られるかもしれず、またそうなってほしいと願っている。

# `Monitor.Wait/Pulse`を置き換えるには
以下はEffective Java 第二版からの引用である。
> 項目69 waitとnotifyよりコンカレンシーユーティリティを選ぶ

Javaの`Object#wait()`, `notify()`はそのままそっくり、.NETの`Monitor.Wait(Object)`, `Pulse(Object)`にあたる。

> `java.util.concurrent`が提供している高級言語に比べると、`wait`と`notify`を直接使用することは、「並行性アセンブリ言語」でプログラミングをしているようなものです。新たなコードで`wait`と`notify`を使用する理由は、あったとしても、めったにありません。

このように、.NETでいうところの`Monitor.Wait(Object)`, `Pulse(Object)`の使用をやめるように記述されている。
その代わりに

> シンクロナイザー(synchronizer)は、スレッドが他のスレッドを待つことを可能にするオブジェクトであり、複数スレッドの活動を協調させることが可能です。

として、シンクロナイザーの使用をすすめている。
そして、特に使用頻度が高いシンクロナイザーの例として、`CountDownLatch`,`Semaphore`を上げている。
Javaの`CountDownLatch`, `Semaphore`に該当するクラスは.NETでは`CountdownEvent`, `Semaphore/SemaphoreSlim`になる。
(使用頻度が低いものとして、`CyclicBarrier`,`Exchanger`も上げられている。前者は.NETでは`Barrier`として存在するが、後者はどうも存在しないようだ。)

`Semaphore/SempahoreSlim`のリソースに同時アクセスするスレッド数を限定する機能は、単純に`Monitor.Wait`を置き換えるだけと考えるとあきらかに余計である。
だから、どちらかと言えば`CountDownLatch`が対象候補になるのだろうが、Javaで使うからといって、.NETに`CountdownEvent`以上に妥当なものが無いとも限らない。

mono(オープンソースの.NET環境)のソースコードを読んでみると、`CountdownEvent`は内部で`ManualResetEventSlim`を使用しているようだ。
`CountdownEvent`のカウントダウンする機能が使用できる場面もあるだろうが、それは毎回ではないのだから、`ManualResetEventSlim`クラスを使用しよう。

# `ManualResetEventSlim`を使用する
`ManualResetEventSlim`は`Set()`によりシグナル状態になり、`Reset()`により非シグナル状態に戻る性質を持つシンクロナイザーだ。
そして、`Wait`メソッドにより、シグナル状態になることをスレッドに待ち受けさせる。また、このメソッドは、タイムアウトの時間と処理のキャンセル用オブジェクト(`CancellationToken`)を受け取ることができる。

実際にこれを使用して、スレッドの実行をブロックするキューをC#で実現してみた。
(今回の例はただの`ManualResetEventSlim`利用サンプルであり、こうした目的にはもちろん`BlockingCollection<T>`を用いるべきだ)

```csharp:BlockingQueue.cs
class BlockingQueue<T>
{
    ~BlockingQueue()
    {
        _resetevent.Dispose();
    }

    private readonly Queue<T> _queue = new Queue<T>();
    private readonly ManualResetEventSlim _resetevent = new ManualResetEventSlim();
    private readonly object _lock = new object();

    public void Put(T data)
    {
        lock (_lock)
        {
            _queue.Enqueue(data);
            _resetevent.Set();
        }
    }

    public T Take(CancellationToken token)
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();

            lock (_lock)
            {                     
                if (_queue.Any())
                {
                    var val = _queue.Dequeue();
                    return val;
                }
                _resetevent.Reset();
            }

            _resetevent.Wait(Timeout.Infinite, token);
        }
    }
}
```

注意が必要だとすれば、`ManualResetEventSlim.Wait`は`lock`のブロックの外で実行しなくてはならない点だ。`Monitor.Wait`と異なり、`ManualResetEventSlim.Wait`は待ち受け中にロックを解放してはくれないからだ。

それをのぞけば、かなり簡潔にわかりやすく記述できる。
ずいぶんな遠回りをしてしまったが、`CancellationTokenSource`で中断可能なブロッキング処理を実現できた。

# `ManualResetEventSlim.Wait`の処理内部
monoのソースコードをざっと斜め読みした際にわかった、`ManualResetEventSlim.Wait`の処理の内部について述べる。

まず、一定期間、ループ文により、

- `Thread.Yield()`
- `Thread.Sleep(0)`
- `Thread.Sleep(1)`
- `CancellationToken.ThrowIfCancellationRequested()`

を実行しながら(つまり、他のスレッドに処理を渡しながら)、シグナル状態かどうかをチェックする。
その後、スピンウェイトはあきらめて、ループの中で、

- `Monitor.Wait(Object, INT32)`
- `CancellationToken.ThrowIfCancellationRequested()`

を実行しながら、シグナル状態かどうかをチェックしている。
この段階では、`Monitor.Wait`によりスレッドの実行を完全に中断している。

応答性が悪くてもかまわない場合、すなわち、スピンウェイトの期間をできるだけ短くして、即、`Monitor.Wait`によるスレッドの中断に移りたい場合は、コンストラクタ`ManualResetEventSlim(Boolean, INT32)`のふたつめの引数にゼロを指定すればいい。
スピンウェイトの期間をできるだけ引き伸ばしたい場合はその逆におおきな値を指定すること。

正直なところ、これを読むまで、`ManualResetEventSlim`は延々とスピンウェイトだけをしているのだとばかり思っていた。そのため、`Monitor.Wait`をこれで置き換えられたとしてもCPUの資源の無駄使いにしかならないと思っていた。ひどい勘違いだった。

# 結論
.NET4.0以降の環境では、`Thread`よりも`Task`を用いるとともに、`Monitor.Wait/Pulse`よりも`ManualResetEventSlim`を使用するべきである。
これにより、単純に`Monitor.Wait`したときよりも応答性がよく、単純にスピンウェイトで待ち受けしたときよりもCPU資源を有効に活用できる上に、`CancellationTokenSource`による処理の中断を実現できる。
また、`ManualResetEventSlim`以外に利用できるシンクロナイザーとして、`CountdownEvent`,`SemaphoreSlim`,`Barrier`が存在する。特定の用途では、`ManualResetEventSlim`よりも簡潔に機能を実現できるだろう。