はじめに
- 次の環境を使用して動作確認しています。
OS Windows 10(64ビット) IDE Microsoft Visual Studio Community 2022(17.1.3) + C#(10.0) - 参考
- こちらのスレッド間の同期を参考にしています。
- リファレンスだと「シグナル」という言葉がでてきますが、スレッドに「通知する」という意味の用語(プロセス・スレッド関連の用語)です。「Set()/Reset()メソッドを使って、セットイベントやリセットイベントをスレッドに通知する。」といった意味で使用されます。
- EventWaitHandle等の詳細な説明は「マネージドスレッド処理」のEventWaitHandleの説明をご覧ください。
EventWaitHandle概要
- EventWaitHandleは、「特定のイベントを待機するためのハンドル(操作のための道具)」であり、複数のスレッド間で待ち合わせを行うためのフラグのようなものです。
- EventWaitHandleはフラグのセット・リセットが可能で、待機側スレッドではフラグがセットされるまで待機することができます。
- 基本的な待ち合わせの流れ(例)を次に示します。
- スレッド間で待ち合わせを行うためのEventWaitHandleを作成し、各スレッドで参照できるようにします。
- 待ち合わせを行うスレッドは、EventWaitHandleオブジェクトのWaitOne()を実行することで、セット状態(シグナル状態)になるまで待機状態になります。
- EventWaitHandleオブジェクトのSet()を実行すると、EventWaitHandleがセット状態(シグナル状態)に変更され、待機状態のスレッドの動作が再開します。
- これらのスレッド群で再び待ち合わせを行う場合、EventWaitHandleオブジェクトの状態をリセットする(非シグナル状態に戻す)必要があります。その方法(EventResetMode)として、自動リセット(AutoReset)と手動リセット(ManualReset)が用意されています。
モード セット後の動作 例えられるモデル 自動リセット 待機スレッドのいずれか1つが開始したら自動的にリセット状態に変更される。結果として同時に開始できるスレッドは1つのみになる。 「回転ドア(turnstile )」
「料金所(tollbooth)」
(一人づつしか通過できない)手動リセット 明示的にリセットされるまでセット状態を継続する。結果として、待機状態の全てのスレッドが開始される。新規に待機を試みるスレッドも待機せずに開始される。
リセットはReset()で行う。「開いたままの門」
(閉じるまで何人でも通過できる。)※EventWaitHandleのコンストラクタでモードを指定できます。
※待機が解除されるスレッドの順番は保証されないことに注意してください。(最初に待機したスレッドが最初に待機解除されるとは限らない。)
AutoResetEventとManualResetEventの違い
- EventWaitHandleオブジェクトの場合、前述のように「自動リセット」または「手動リセット」のいずれかのモードを指定する必要があります。
「自動リセット」を指定したEventWaitHandleと同じ動作をするのがAutoResetEvent、「手動リセット」を指定したEventWaitHandleと同じ動作をするのがManualResetEventです。 - ManualResetEventに関しては、より軽量な実装となるManualResetEventSlimクラスも用意されています。(xxxSlimという軽量実装クラスはOSの機能を使用しない実装になっており性能が向上しますが、プロセス間での同期には使用できません。)
AutoResetEventの使い方
- AutoResetEventは、前述のEventWaitHandleを継承しており、EventWaitHandleの自動リセットと同じ動作を行います。
- AutoResetEventを使ったスレッド同期
- AutoResetEventオブジェクトがリセット状態(非シグナル状態)の場合、WaitOne()メソッドを実行したスレッドは待機状態になります。
- Set()メソッドの呼び出しによりAutoResetEventオブジェクトがセット状態(シグナル状態)になると、いずれか一つのスレッドの待機が解除され、その後は自動的にリセット状態になります。
- 結果として、Set()メソッドの1回の実行に対して1つのスレッドのみの待機解除が可能です。
- AutoResetEventを使ったスレッド同期の例(フロー)
- 子スレッド(Thread-1, Thread-2, Thread-3)がWaitOne()メソッドで待機しています。
- 親スレッドのSet()メソッド実行で、任意の1つのスレッド(この例ではThread-2)の待機が解除されます。
- 同様に、親スレッドでSet()メソッドの実行を繰り返し、残りスレッド(Thread-1, Thread-3)の待機を順番に解除します。
- サンプルを次に示します。
Visual Studioプロジェクトやソースコードはこちらから確認できます。public class AutoResetEventExamples { private AutoResetEvent _event = new AutoResetEvent(false); public void DoExample() { Console.WriteLine("サンプル開始"); Task.Run(() => ThreadProc("thread-1")); Task.Run(() => ThreadProc("thread-2")); Task.Run(() => ThreadProc("thread-3")); Thread.Sleep(1000); Console.WriteLine("Set(1回目)"); _event.Set(); // 任意の1スレッドが待機解除 Thread.Sleep(1000); Console.WriteLine("Set(2回目)"); _event.Set(); // 任意の1スレッドが待機解除 Thread.Sleep(1000); Console.WriteLine("Set(3回目)"); _event.Set(); // 任意の1スレッドが待機解除 Thread.Sleep(1000); Console.WriteLine("サンプル終了"); } private void ThreadProc(string name) { Console.WriteLine($" {name}: シグナルを待機中..."); _event.WaitOne(); Console.WriteLine($" {name}: 終了"); } }
- サンプルの実行結果の例を次に示します。
インデントがない出力は親スレッド、インデントがある出力は子スレッドのものです。サンプル開始 thread-1: シグナルを待機中... thread-2: シグナルを待機中... thread-3: シグナルを待機中... Set(1回目) thread-2: 終了 Set(2回目) thread-1: 終了 Set(3回目) thread-3: 終了 サンプル終了
ManualResetEventの使い方
- ManualResetEventは、前述のEventWaitHandleを継承しており、EventWaitHandleの手動リセットと同じ動作を行います。
- ManualResetEventを使ったスレッド同期
- AutoResetEventオブジェクトと同様に、ManualResetEventオブジェクトがリセット状態(非シグナル状態)の場合、WaitOne()メソッドを実行したスレッドは待機状態になります。
- Set()メソッドの呼び出しによりManualResetEventオブジェクトがセット状態(シグナル状態)になると、全てのスレッドの待機が解除されます。
- Reset()メソッドが実行されるまでセット状態(シグナル状態)は継続するため、後続のスレッドがWaitOne()を実行しても待機しません。
- 結果として、Set()メソッドの1回の実行に対してN個のスレッドの待機解除が可能です。Reset()が呼び出されるまで待機状態になりません。
- AutoResetEventを使ったスレッド同期の例(フロー)
- 子スレッド(Thread-1, Thread-2)がWaitOneメソッドで待機しています。
- 親スレッドのSetメソッド実行で、待機中の全てのスレッド(Thread-1, Thread-2)の待機が解除されます。
- まだセット状態(シグナル状態)なので、WaitOneメソッドで待機するスレッド(Thread-3)は、待機状態になりません。
- Reset()の実行以後、WaitOne()メソッドを実行するスレッド(Thread-4)は待機状態になります。
- サンプルを次に示します。
Visual Studioプロジェクトやソースコードはこちらから確認できます。public class ManualResetEventExamples { private ManualResetEvent _event = new ManualResetEvent(false); public void DoExample() { Console.WriteLine("サンプル開始"); Task.Run(() => ThreadProc("thread-1")); // シグナル待機 Task.Run(() => ThreadProc("thread-2")); // シグナル待機 Thread.Sleep(1000); Console.WriteLine("Set"); _event.Set(); // 以後、Reset()するまでWaitOne()は待機しない Thread.Sleep(1000); Task.Run(() => ThreadProc("thread-3")); // シグナル非待機 Thread.Sleep(1000); Console.WriteLine("Reset"); _event.Reset(); Task.Run(() => ThreadProc("thread-4")); // シグナル待機 Thread.Sleep(1000); Console.WriteLine("Set"); _event.Set(); Thread.Sleep(1000); Console.WriteLine("サンプル終了"); } private void ThreadProc(string name) { Console.WriteLine($" {name}: シグナルを待機中..."); _event.WaitOne(); Console.WriteLine($" {name}: 終了"); } }
- サンプルの実行結果の例を次に示します。
インデントがない出力は親スレッド、インデントがある出力は子スレッドのものです。サンプル開始 thread-2: シグナルを待機中... thread-1: シグナルを待機中... Set thread-1: 終了 thread-2: 終了 thread-3: シグナルを待機中... thread-3: 終了 Reset thread-4: シグナルを待機中... Set thread-4: 終了 サンプル終了