調査内容
C#かつWindows向けプログラムではWaitHandleを待ちたいことがある。例えばWaitableTimerの満了待機のために。これはTaskともまた違う設計思想に基づいているように見える。よくわからないので調べた。特に待機時のスレッドについて。
WaitHandleまわり
- イベントとはシグナル状態、非シグナル状態の2値によるタイミング通知の仕組み。
- イベント待機にはWaitHandleを使う。待機中はスレッドブロッキングする。
- イベント操作にはEventWaitHandle/AutoResetEvent/ManualResetEventクラスを使う。
- できあいのクラスはこれらによる操作機能をすでに内包している。
- WaitAny/WaitAllはWaitForMultipleObjects関数相当のC#版
参考資料
ブロッキング対象スレッド
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メソッドを呼ぶ。
なおスレッド生成タイミングや生成数にも複数のやり方がある。WaitHandle1つ毎に都度生成&破棄、起動時に固定数生成、負荷に応じて数を自動調整など。
キャンセル
WaitHandle.WaitOneメソッドは対象が条件を満たすまでずっと待機し続ける。これを中断するには2つの方法がある。
- WaitOneメソッドの引数にタイムアウト時間を指定する。
- 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によるキャンセル。
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
new TimeSpan(0, 0, 20));
必要スレッド数削減
WaitOneは1つのWaitHandle待機につき1スレッドを使ってしまう。
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/
参考資料
The value of MAXIMUM_WAIT_OBJECTS is 64 (defined in winnt.h),
- 和訳がわかりにくい場合は英語版を見る: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerwaitforsingleobject
→ThreadPoolクラスのI/Oスレッドについての解説有り。どうもIOCP: I/O Completion Portのためのスレッドということらしい。それ以外はI/O待ちだろうとなんだろうとThreadPoolのワーカースレッドを使う。