我如何等待C#中的事件?

Joh*_*ger 65 c# events asynchronous

我正在创建一个包含一系列事件的类,其中一个是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());
}
Run Code Online (Sandbox Code Playgroud)

活动报名

// 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);
Run Code Online (Sandbox Code Playgroud)

我知道事件的签名是void EventName,所以使它异步基本上是火和忘记.我的引擎大量使用事件来通知第三方开发人员(和多个内部组件)事件正在引擎内发生并让他们对它们做出反应.

是否有一个很好的途径可以使用我可以使用的异步方式来替换事件?我不确定我是否应该使用BeginShutdownGameEndShutdownGame回调,但这是一个痛苦,因为那时只有调用源可以传递回调,而不是任何插入引擎的第三方东西,这是我得到的事件.如果服务器调用game.ShutdownGame(),引擎中的引擎插件和/或其他组件无法传递回调,除非我连接某种注册方法,保留一组回调.

任何关于优先/推荐的路线的建议将非常感谢!我环顾四周,大部分时间我看到的是使用开始/结束方法,我认为这不会满足我想做的事情.

编辑

我正在考虑的另一个选择是使用注册方法,这需要一个等待回调.我遍历所有的回调,抓住他们的任务并等待一个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);
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*iho 77

就个人而言,我认为拥有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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

通过这个例子,我现在发现自己想知道是否有一种方法可以让C#抽象出来.也许这将是一个太复杂的变化,但旧式 - void返回事件处理程序和新async/ await功能的当前组合似乎有点尴尬.上面的工作(并且运行良好,恕我直言),但是对于场景具有更好的CLR和/或语言支持(即能够等待多播委托并让C#编译器将其转换为调用WhenAll())会很不错..

  • 对于调用所有订阅者的部分,您可以使用LINQ:`await Task.WhenAll(handler.GetInvocationList().Select(invocation =>((Func <object,EventArgs,Task>)调用)(this,EventArgs.Empty) ));` (3认同)
  • FWIW,我把[AsyncEvent](https://github.com/TAGC/AsyncEvent)放在一起,基本上是彼得建议的.这是一个权宜之计,直到微软实施适当的支持. (2认同)

Den*_*Den 8

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Example
{
    // delegate as alternative standard EventHandler
    public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token);


    public class ExampleObject
    {
        // use as regular event field
        public event AsyncEventHandler<EventArgs> AsyncEvent;

        // invoke using the extension method
        public async Task InvokeEventAsync(CancellationToken token) {
            await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token);
        }

        // subscribe (add a listener) with regular syntax
        public static async Task UsageAsync() {
            var item = new ExampleObject();
            item.AsyncEvent += (sender, e, token) => Task.CompletedTask;
            await item.InvokeEventAsync(CancellationToken.None);
        }
    }


    public static class AsynEventHandlerExtensions
    {
        // invoke a async event (with null-checking)
        public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) {
            var delegates = handler?.GetInvocationList();
            if (delegates?.Length > 0) {
                var tasks = delegates
                    .Cast<AsyncEventHandler<TEventArgs>>()
                    .Select(e => e.Invoke(sender, args, token));
                await Task.WhenAll(tasks);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Kos*_*sky 7

Peter 的例子很棒,我只是使用 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

添加超时可能是个好主意。要引发事件调用 Raise 扩展:

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

private async Task SomeMethod()
{
    ...

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

    ...
}
Run Code Online (Sandbox Code Playgroud)

但您必须注意,与同步事件不同,此实现并发调用处理程序。如果处理程序必须严格连续执行它们经常做的事情,例如下一个处理程序取决于前一个处理程序的结果,这可能是一个问题:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

您最好将扩展方法更改为连续调用处理程序:

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)