web-dev-qa-db-ja.com

C#でイベントを待機するにはどうすればよいですか?

一連のイベントを持つクラスを作成していますが、そのうちの1つがGameShuttingDownです。このイベントが発生したら、イベントハンドラーを呼び出す必要があります。このイベントのポイントは、ユーザーにゲームがシャットダウンされ、データを保存する必要があることを通知することです。保存は待機可能ですが、イベントは待機できません。そのため、ハンドラーが呼び出されると、待機中のハンドラーが完了する前にゲームがシャットダウンします。

public event EventHandler<EventArgs> GameShuttingDown;

public virtual async Task ShutdownGame()
{
    await this.NotifyGameShuttingDown();

    await this.SaveWorlds();

    this.NotifyGameShutDown();
}

private async Task SaveWorlds()
{
    foreach (DefaultWorld world in this.Worlds)
    {
        await this.worldService.SaveWorld(world);
    }
}

protected virtual void NotifyGameShuttingDown()
{
    var handler = this.GameShuttingDown;
    if (handler == null)
    {
        return;
    }

    handler(this, new EventArgs());
}

イベント登録

// The game gets shut down before this completes because of the nature of how events work
DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);

イベントのシグネチャはvoid EventNameであるため、非同期にすることは基本的には忘れてしまいます。私のエンジンは、イベントがエンジン内で発生し、それらに反応することをサードパーティの開発者(および複数の内部コンポーネント)に通知するために、イベンティングを多用しています。

イベンティングを非同期ベースのものに置き換えて使用できる良いルートはありますか?コールバックでBeginShutdownGameおよびEndShutdownGameを使用する必要があるかどうかはわかりませんが、それは苦痛です。なぜなら、呼び出し元のみがコールバックを渡すことができ、サードパーティのものはエンジン、これは私がイベントで得ているものです。サーバーがgame.ShutdownGame()を呼び出す場合、コールバックのコレクションを保持して何らかの登録方法を確立しない限り、エンジンプラグインやエンジン内の他のコンポーネントがコールバックを渡す方法はありません。

これを使用するための優先/推奨ルートに関するアドバイスは大歓迎です!私は周りを見回しましたが、私が見たほとんどの部分は、私がやりたいことを満足させるとは思わないBegin/Endアプローチを使用しています。

編集

私が検討している別のオプションは、待機メソッドのコールバックを取る登録方法を使用することです。すべてのコールバックを反復処理し、タスクを取得してWhenAllで待機します。

private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();

public void RegisterShutdownCallback(Func<Task> callback)
{
    this.ShutdownCallbacks.Add(callback);
}

public async Task Shutdown()
{
    var callbackTasks = new List<Task>();
    foreach(var callback in this.ShutdownCallbacks)
    {
        callbackTasks.Add(callback());
    }

    await Task.WhenAll(callbackTasks);
}
61

個人的には、asyncイベントハンドラーを持つことは、設計上の最善の選択ではないかもしれません。少なくともその理由は、あなたが抱えている問題そのものです。同期ハンドラーでは、いつ完了するかを知るのは簡単です。

とはいえ、何らかの理由でこの設計に固執する必要があるか、少なくとも強く強いられている場合は、awaitに優しい方法でそれを行うことができます。

ハンドラーとawaitを登録するのは良い考えです。ただし、コード内のイベントの表現力を維持するため、既存のイベントパラダイムに固執することをお勧めします。主なことは、標準のEventHandlerベースのデリゲート型から逸脱し、Taskを返すデリゲート型を使用して、ハンドラーをawaitできるようにする必要があることです。

ここに私が意味することを示す簡単な例があります:

class A
{
    public event Func<object, EventArgs, Task> Shutdown;

    public async Task OnShutdown()
    {
        Func<object, EventArgs, Task> handler = Shutdown;

        if (handler == null)
        {
            return;
        }

        Delegate[] invocationList = handler.GetInvocationList();
        Task[] handlerTasks = new Task[invocationList.Length];

        for (int i = 0; i < invocationList.Length; i++)
        {
            handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
        }

        await Task.WhenAll(handlerTasks);
    }
}

OnShutdown()メソッドは、標準の「イベントデリゲートインスタンスのローカルコピーの取得」を実行した後、最初にすべてのハンドラーを呼び出し、次に返されたすべてのTasksを待機します(ハンドラーとしてローカル配列に保存します)呼び出されます)。

使用方法を示す短いコンソールプログラムを次に示します。

class Program
{
    static void Main(string[] args)
    {
        A a = new A();

        a.Shutdown += Handler1;
        a.Shutdown += Handler2;
        a.Shutdown += Handler3;

        a.OnShutdown().Wait();
    }

    static async Task Handler1(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #1");
        await Task.Delay(1000);
        Console.WriteLine("Done with shutdown handler #1");
    }

    static async Task Handler2(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #2");
        await Task.Delay(5000);
        Console.WriteLine("Done with shutdown handler #2");
    }

    static async Task Handler3(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #3");
        await Task.Delay(2000);
        Console.WriteLine("Done with shutdown handler #3");
    }
}

この例を経て、C#でこれを少し抽象化する方法がなかったのではないかと思うようになりました。複雑すぎる変更だったかもしれませんが、古いスタイルのvoid- returningイベントハンドラーと新しいasync/await機能の現在の組み合わせは少し厄介に思えます。上記は動作します(そして、うまく動作します、私見)、しかし、シナリオのためにより良いCLRおよび/または言語サポートがあれば良かったでしょう(すなわち、マルチキャストデリゲートを待ち、C#コンパイラにWhenAll()の呼び出しに変換させることができます) )。

74
Peter Duniho
internal static class EventExtensions
{
    public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @event, object sender,
        TEventArgs args, AsyncCallback ar, object userObject = null)
        where TEventArgs : class
    {
        var listeners = @event.GetInvocationList();
        foreach (var t in listeners)
        {
            var handler = (EventHandler<TEventArgs>) t;
            handler.BeginInvoke(sender, args, ar, userObject);
        }
    }
}

例:

    public event EventHandler<CodeGenEventArgs> CodeGenClick;

        private void CodeGenClickAsync(CodeGenEventArgs args)
    {
        CodeGenClick.InvokeAsync(this, args, ar =>
        {
            InvokeUI(() =>
            {
                if (args.Code.IsNotNullOrEmpty())
                {
                    var oldValue = (string) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code));
                    if (oldValue != args.Code)
                        gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code);
                }
            });
        });
    }

注:これは非同期であるため、イベントハンドラーはUIスレッドを侵害する可能性があります。イベントハンドラー(サブスクライバー)はUI作業を実行しないでください。それ以外の場合はあまり意味がありません。

  1. イベントプロバイダーでイベントを宣言します。

    パブリックイベントEventHandler DoSomething;

  2. プロバイダーのイベントを呼び出します:

    DoSomething.InvokeAsync(new MyEventArgs()、this、ar => {終了時にコールバックが呼び出されます(ここで必要なときにUIを同期してください!)}、null);

  3. 通常のようにクライアントによってイベントをサブスクライブします

2

確かに、イベントは本質的に待ち切れないので、回避する必要があります。

過去に使用した解決策の1つは、 セマフォ を使用して、その中のすべてのエントリが解放されるのを待つことです。私の状況では、サブスクライブされたイベントは1つしかなかったので、new SemaphoreSlim(0, 1)としてハードコードできますが、イベントのゲッター/セッターをオーバーライドして、動的にできるようにサブスクライバーの数のカウンターを保持することができます同時スレッドの最大量を設定します。

その後、セマフォエントリを各サブスクライバに渡し、SemaphoreSlim.CurrentCount == amountOfSubscribers(別名:すべてのスポットが解放されるまで)を実行させます。

これは、すべてのイベントサブスクライバーが終了するまで、プログラムを本質的にブロックします。

サブスクライバーにGameShutDownFinishedイベントを提供することを検討することもできます。これらのイベントは、ゲーム終了タスクが完了したときに呼び出す必要があります。 SemaphoreSlim.Release(int)オーバーロードと組み合わせることで、すべてのセマフォエントリを消去し、Semaphore.Wait()を使用してスレッドをブロックできます。すべてのエントリがクリアされたかどうかをチェックする代わりに、1つのスポットが解放されるまで待機します(ただし、すべてのスポットが一度に解放されるのは1瞬間だけです)。

1
Jeroen Vannevel

Peter's 例は素晴らしいです。LINQと拡張機能を使用して、少し単純化しました。

public static class AsynchronousEventExtensions
{
    public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            return Task.WhenAll(handlers.GetInvocationList()
                .OfType<Func<TSource, TEventArgs, Task>>()
                .Select(h => h(source, args)));
        }

        return Task.CompletedTask;
    }
}

タイムアウトを追加することをお勧めします。イベントを呼び出すには、Raise拡張機能を呼び出します。

public event Func<A, EventArgs, Task> Shutdown;

private async Task SomeMethod()
{
    ...

    await Shutdown.Raise(this, EventArgs.Empty);

    ...
}

ただし、同期イベントとは異なり、この実装ではハンドラーが同時に呼び出されることに注意する必要があります。ハンドラーが頻繁に行うことを厳密に連続して実行する必要がある場合、問題になる可能性があります。次のハンドラーは、前のハンドラーの結果に依存します。

someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;

...

private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
    // OnShutdown2 will start execution the moment OnShutdown1 hits await
    // and will proceed to the operation, which is not the desired behavior.
    // Or it can be just a concurrent DB query using the same connection
    // which can result in an exception thrown base on the provider
    // and connection string options
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

拡張メソッドを変更して、ハンドラーを連続して呼び出すことをお勧めします。

public static class AsynchronousEventExtensions
{
    public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
            {
                await handler(source, args);
            }
        }
    }
}
1
Kosta_Arnorsky

私はこのために非同期とタスクの使用について具体的に尋ねていたことを知っていますが、ここではハンドラが値を返す必要がないことを意味する代替案があります。コードはPeter Dunihoの例に基づいています。最初に同等のクラスA(少し収まるように押しつぶします):-

class A
{
    public delegate void ShutdownEventHandler(EventArgs e);
    public event ShutdownEventHandler ShutdownEvent;
    public void OnShutdownEvent(EventArgs e)
    {
        ShutdownEventHandler handler = ShutdownEvent;
        if (handler == null) { return; }
        Delegate[] invocationList = handler.GetInvocationList();
        Parallel.ForEach<Delegate>(invocationList, 
            (hndler) => { ((ShutdownEventHandler)hndler)(e); });
    }
}

使い方を示すシンプルなコンソールアプリケーション...

using System;
using System.Threading;
using System.Threading.Tasks;

...

class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        a.ShutdownEvent += Handler1;
        a.ShutdownEvent += Handler2;
        a.ShutdownEvent += Handler3;
        a.OnShutdownEvent(new EventArgs());
        Console.WriteLine("Handlers should all be done now.");
        Console.ReadKey();
    }
    static void handlerCore( int id, int offset, int num )
    {
        Console.WriteLine("Starting shutdown handler #{0}", id);
        int step = 200;
        Thread.Sleep(offset);
        for( int i = 0; i < num; i += step)
        {
            Thread.Sleep(step);
            Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num);
        }
        Console.WriteLine("Done with shutdown handler #{0}", id);
    }
    static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); }
    static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); }
    static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); }
}

これが誰かに役立つことを願っています。

1
jetbadger