【C#】複数の名前付きイベントのシグナル状態を監視(WaitForMultipleObjects)する

# 背景

C++で作成されたプロセスでイベントを発火し、C#で作成されたプロセスでそのイベント発火を補足したい（通知を受け取りたい）ことがあり、C#でイベントをどう扱うのか調べたので残しておきます。

# 名前付きイベント

プロセスやスレッド間で同期をとったり、何かを通知したりするのにイベントという仕組みを使用することができます。
仕組みとしてはイベントオブジェクトを作成し、そのオブジェクトの状態を監視するというものです。
イベントオブジェクトはシグナル状態もしくは非シグナル状態の2種類の状態を持ちます。

参考：[イベント オブジェクトの使用 (同期) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/sync/using-event-objects)

## C++の各種API

イベントを使用する際の基本的な流れは以下の感じです。
1. イベントオブジェクトの作成
1. イベントオブジェクトのハンドル取得
    - イベントを作成した場所以外でハンドルを取得したい場合（名前付きで別プロセスからのアクセスなど）は、イベントオープンでハンドル取得が必要
1. イベントの待機

C++だと以下のAPIを組み合わせて実現することができます。

- [CreateEvent](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-createeventa)：イベントオブジェクトを作成する（名前も付与できる）
    - プロセス間で同期や通知を取りたい場合はイベントオブジェクトに名前を付けて、同期をとりたいプロセス同士でその名前が付いた1つのイベントオブジェクトでやり取りをおこないます。
        - 参考：https://learn.microsoft.com/ja-jp/windows/win32/sync/using-named-objects
    - 名前に`Global\\`プレフィックスをつけるとグローバルなイベントオブジェクトとなり、ユーザーをまたがったイベントオブジェクトとなり、イベントオブジェクトを作成したユーザー以外からもその名前でイベントオブジェクトを開くことができる
        - NamedPipeと同じようなイメージです
- [OpenEvent](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-openeventa)：名前付きイベントを開いてイベントオブジェクトのハンドルを取得する
- [SetEvent](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-setevent)：イベントオブジェクトをシグナル状態にする
- [ResetEvent](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-resetevent)：イベントオブジェクトを非シグナル状態にする
- [WaitForSingleObjects](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject)：指定したイベントオブジェクトがシグナル状態になるまで待機する
- [WaitForMultipleObjects](https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects)：1つ以上のイベントオブジェクトがシグナル状態になるまで待機する

# EventWaitHandleクラス

[EventWaitHanlde](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.eventwaithandle?view=net-7.0)クラスでC++のイベントAPI群と同等のことを実現できます。
[WaitHandle](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.waithandle?view=net-7.0)クラスが待機関連のstaticメソッドを持っていて、EventWaitHandleクラスがそれを実現しています。
名前付きのイベントオブジェクトを作成したい場合はEventWaitHandleクラスのコンストラクタで指定してあげればよいです。

[AutoResetEvent]()クラスはEventWaitHandleクラスで[EventResetMode](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.eventresetmode?view=net-7.0)をAutoReset（シグナルを受信すると自動的にリセットする）にしたもの、[ManualResetEvent](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.manualresetevent?view=net-7.0)はManualReset（シグナルを受信してもシグナル状態のまま）にしたものとなっています。
AutoResetEventクラスとManualResetEventクラスは名前を受け取るコンストラクタがないので、名前付きイベントの作成はできません。

```plantuml
WaitHandle <|-- EventWaitHandle
EventWaitHandle <|-- AutoResetEvent
EventWaitHandle <|-- ManualResetEvent

class WaitHandle {
+ WaitTimeOut: const int = 258
+ {static} SignalAndWait(WaitHandle[], ...): bool
+ {static} WaitAny(WaitHandle[], ...): int
+ {static} WaitAll(WaitHandle[], ...): bool
+ WaitOne() virtual: bool
}

class EventWaitHandle {
+ EventWaitHandle(bool, EventResetMode)
+ EventWaitHandle(bool, EventResetMode, string)
+ EventWaitHandle(bool, EventResetMode, string, bool)
+ Set(): bool
+ Reset(): bool
+ {static} TryOpenExisting(string, EventWaitHandle): bool
}

class AutoResetEvent {
+ AutoResetEvent(bool)
}

class ManualResetEvent {
+ ManualResetEvent(bool)
}
```

# C#で監視中の複数イベントのうち1つでも発火したことを補足する例(WaitForMultipleObjects関数相当)

`EventWaitHandle`クラスの`WaitAny`メソッドを使用して実現することができます。。
WaitForMultipleObjectsと似たように書き方で、イベントハンドルの配列をメソッドに渡し、発火したイベントの配列インデックスが戻り値として返されるので、そのイベントに応じた処理を記述すればよいです。
以下は外部のプロセスがイベントを発火し、補足されたイベントによって処理を実行するサンプルです。

```cs
void EventWait()
{
    var created = Enumerable.Repeat(true, 3).ToArray();
    // 名前付きイベントを作成
    var events = new[]
    {
        new EventWaitHandle(false, EventResetMode.ManualReset, "EventName_Hoge", out created[0]),
        new EventWaitHandle(false, EventResetMode.ManualReset, "EventName_Fuga", out created[1]),
        new EventWaitHandle(false, EventResetMode.ManualReset, "EventName_Finish", out created[2]),
    };

    if (created.Any(x => !x))
    {
        Array.ForEach(events, x => x.Dispose());
        return;
    }

    // イベント発火を何度でも補足できるようにループする
    var isLoop = true;
    while (isLoop)
    {
        try
        {
            // イベント発火が補足されるまで待機
            var signaledIdx = WaitHandle.WaitAny(events);
            switch (signaledIdx)
            {
                // Hogeイベント
                case 0:
                    // Hogeイベント受信時の処理
                    events[0].Reset(); // 再度Hogeイベントを補足できるように非シグナル状態に
                    break;

                // Fugaイベント
                case 1:
                    // Fugaイベント受信時の処理
                    events[1].Reset(); // 再度Fugaイベントを補足できるように非シグナル状態に
                    break;

                // Finishイベント
                case 2:
                    isLoop = false; // ループを抜けて終了
                    events[2].Reset();
                    break;

                default:
                    isLoop = false;
                    break;
            }
        }
        catch
        {
            isLoop = false;
        }
    }
    Array.ForEach(events, x => x.Dispose());
}
```