C#でのWaitHandle待機スレッド調査

# 調査内容

C#かつWindows向けプログラムではWaitHandleを待ちたいことがある。例えばWaitableTimerの満了待機のために。これはTaskともまた違う設計思想に基づいているように見える。よくわからないので調べた。特に待機時のスレッドについて。

# WaitHandleまわり

- イベントとはシグナル状態、非シグナル状態の２値によるタイミング通知の仕組み。
- イベント待機にはWaitHandleを使う。待機中はスレッドブロッキングする。
- イベント操作にはEventWaitHandle/AutoResetEvent/ManualResetEventクラスを使う。
    - できあいのクラスはこれらによる操作機能をすでに内包している。
- WaitAny/WaitAllはWaitForMultipleObjects関数相当のC#版

### 参考資料

https://qiita.com/nobushi95/items/d98da344c87a6c9d50c7

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.waithandle?view=net-8.0

# ブロッキング対象スレッド

WaitHandleの待機中はスレッドがブロッキングする。対象スレッド毎に待機方法をまとめる。

## スレッドの分類、生成方法別

| No | ブロッキング対象スレッド案 | どのようなプログラムでブロッキング可能か |
|:-|:-|:-|
| 1 | メインスレッド | バッチ処理。 |
| 2 | ThreadPoolワーカースレッド | ThreadPoolが暇なプログラム。並列度が低いもの。 |
| 3 | ThreadPool I/Oスレッド | 対象外とする。これはIOCP用。それ以外の用途にはWaitHandle含め適さない。 |
| 4 | 明示的に生成したスレッド | ThreadPoolが忙しいプログラム。並列度が高いもの。 |

## スレッド別、待機実現方法

### メインスレッド

- メインスレッドでWaitHandle.WaitOneメソッドを呼ぶ。

### ThreadPoolワーカースレッド

- Task.RunとWaitHandle.WaitOneメソッドを組み合わせる。
- ThreadPool.QueueUserWorkItemとWaitHandle.WaitOneメソッドを組み合わせる。
- ThreadPool.RegisterWaitForSingleObjectメソッドを使う。

### 明示的に生成したスレッド

- `new Thread()`, `_beginthread()`, `pthread_create()`などでスレッドを生成して利用する。それらのスレッド中でWaitHandle.WaitOneメソッドを呼ぶ。

なおスレッド生成タイミングや生成数にも複数のやり方がある。WaitHandle１つ毎に都度生成＆破棄、起動時に固定数生成、負荷に応じて数を自動調整など。

# キャンセル

WaitHandle.WaitOneメソッドは対象が条件を満たすまでずっと待機し続ける。これを中断するには２つの方法がある。

1. WaitOneメソッドの引数にタイムアウト時間を指定する。
2. WaitHandle.WaitAllまたはWaitHandle.WaitAnyを使ってその他のWaitHandleとともに待機する。例えばCancellationToken.WaitHandle.

これらは併用もできる。下記の記事に実装コード例有り。

- https://learn.microsoft.com/en-us/dotnet/standard/threading/how-to-listen-for-cancellation-requests-that-have-wait-handles
- https://thomaslevesque.com/2015/06/04/async-and-cancellation-support-for-wait-handles/
- https://stackoverflow.com/questions/55987930/how-to-wait-for-multiple-waithandle-array-in-async-way-in-c-sharp-with-cancellat

コード： 上記記事から引用、CancellationTokenによるキャンセル。

```csharp
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));
```

# 必要スレッド数削減

WaitOneは１つのWaitHandle待機につき１スレッドを使ってしまう。

ThreadPool.RegisterWaitForSingleObjectはその内部でWaitForMultipleObjectsを使っているため必要スレッド数が1/64で済むとのこと（※）。自前スレッドプールの場合も同様の対応で必要スレッド数を削減できる。

※参考： https://devblogs.microsoft.com/oldnewthing/20081117-00/?p=20183

WaitForMultipleObjects関数は同時に取り扱い可能なハンドル数が64までなことに注意。これはWaitHandle.WaitAllとWaitHandle.WaitAnyでも同様。

# Taskでラッピング

WaitHandleのまま扱うのではなくTaskとして扱う方法もある。

下記の記事にThreadPool.RegisterWaitForSingleObjectをTaskCompletionSourceでラッピングするコード例が載っている。

- https://stackoverflow.com/questions/34548740/how-to-make-a-nonblocking-wait-handle
- https://thomaslevesque.com/2015/06/04/async-and-cancellation-support-for-wait-handles/

# 参考資料

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.waithandle.waitany?view=net-8.0

https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects

https://stackoverflow.com/questions/39380131/why-does-waitformultipleobjects-fail-with-multiple-thread-handles
> The value of MAXIMUM_WAIT_OBJECTS is 64 (defined in winnt.h),

https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-registerwaitforsingleobject

- 和訳がわかりにくい場合は英語版を見る： https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerwaitforsingleobject

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.threadpool.registerwaitforsingleobject?view=net-8.0

https://stackoverflow.com/questions/2099947/simple-description-of-worker-and-i-o-threads-in-net

→ThreadPoolクラスのI/Oスレッドについての解説有り。どうもIOCP: I/O Completion Portのためのスレッドということらしい。それ以外はI/O待ちだろうとなんだろうとThreadPoolのワーカースレッドを使う。