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

How do I await events in C#?

我正在创建一个包含一系列事件的 class,其中之一是 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 所以让它异步基本上是即发即忘。我的引擎大量使用事件来通知 3rd 方开发人员(和多个内部组件)引擎内正在发生事件并让他们对它们做出反应。

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

任何关于 preferred/recommended 路线的建议都将不胜感激!我环顾四周,在大多数情况下,我所看到的是使用 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);
}

的确如此,事件本质上是不可等待的,因此您必须解决它。

我过去使用的一个解决方案是使用 a semaphore 等待其中的所有条目被释放。在我的情况下,我只有一个订阅的事件,所以我可以将它硬编码为 new SemaphoreSlim(0, 1) 但在你的情况下,你可能想为你的事件覆盖 getter/setter 并记录有多少订阅者,所以你可以动态设置并发线程的最大数量。

之后,您将一个信号量条目传递给每个订阅者并让他们做他们的事情,直到 SemaphoreSlim.CurrentCount == amountOfSubscribers(又名:所有点都已被释放)。

这实际上会阻止您的程序,直到所有事件订阅者都完成。

您可能还想考虑为您的订阅者提供一个类似 GameShutDownFinished 的事件,他们必须在完成游戏结束任务时调用该事件。结合 SemaphoreSlim.Release(int) 重载,您现在可以清除所有信号量条目并简单地使用 Semaphore.Wait() 来阻塞线程。您现在不必检查是否所有条目都已被清除,而是等到一个点被释放(但应该只有一个时刻所有点被一次释放)。

就我个人而言,我认为拥有 async 事件处理程序可能不是最佳设计选择,其中最重要的原因就是您遇到的问题。使用同步处理程序,知道它们何时完成是微不足道的。

就是说,如果出于某种原因您必须或至少被迫坚持使用此设计,您可以以 await 友好的方式进行。

您注册处理程序的想法 await 他们是个好主意。但是,我建议坚持使用现有的事件范例,因为这将保持代码中事件的表现力。最主要的是你必须偏离标准的基于 EventHandler 的委托类型,并使用 returns 和 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()方法,在完成标准"get local copy of the event delegate instance"之后,首先调用所有的处理程序,然后等待所有返回的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-返回事件处理程序和新的 async/await 功能似乎有点尴尬。上面的工作(并且工作得很好,恕我直言),但是如果有更好的 CLR and/or 语言支持场景(即能够等待多播委托并让 C# 编译器将其转换为调用至 WhenAll()).

我知道该操作专门询问有关为此使用异步和任务的问题,但这里有一个替代方案,这意味着处理程序不需要 return 一个值。该代码基于 Peter Duniho 的示例。首先是等效的 class 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); }
}

我希望这对某人有用。

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-work。 否则就没有多大意义了。

  1. 在您的活动提供商中声明您的活动:

    public 事件 EventHandler DoSomething;

  2. 调用您的提供商的事件:

    DoSomething.InvokeAsync(new MyEventArgs(), this, ar => { 完成时调用的回调(此处需要时同步 UI!)}, null);

  3. 像往常一样由客户订阅活动

示例很棒,我只是使用 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);
            }
        }
    }
}

如果您需要等待标准的 .net 事件处理程序,您不能这样做,因为它是 void

但是您可以创建一个异步事件系统来处理:

public delegate Task AsyncEventHandler(AsyncEventArgs e);

public class AsyncEventArgs : System.EventArgs
{
    public bool Handled { get; set; }
}

public class AsyncEvent
{
    private string name;
    private List<AsyncEventHandler> handlers;
    private Action<string, Exception> errorHandler;

    public AsyncEvent(string name, Action<string, Exception> errorHandler)
    {
        this.name = name;
        this.handlers = new List<AsyncEventHandler>();
        this.errorHandler = errorHandler;
    }

    public void Register(AsyncEventHandler handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Add(handler);
    }

    public void Unregister(AsyncEventHandler handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Remove(handler);
    }

    public IReadOnlyList<AsyncEventHandler> Handlers
    {
        get
        {
            var temp = default(AsyncEventHandler[]);

            lock (this.handlers)
                temp = this.handlers.ToArray();

            return temp.ToList().AsReadOnly();
        }
    }

    public async Task InvokeAsync()
    {
        var ev = new AsyncEventArgs();
        var exceptions = new List<Exception>();

        foreach (var handler in this.Handlers)
        {
            try
            {
                await handler(ev).ConfigureAwait(false);

                if (ev.Handled)
                    break;
            }
            catch(Exception ex)
            {
                exceptions.Add(ex);
            }
        }

        if (exceptions.Any())
            this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
    }
}

你现在可以声明你的异步事件了:

public class MyGame
{
    private AsyncEvent _gameShuttingDown;

    public event AsyncEventHandler GameShuttingDown
    {
        add => this._gameShuttingDown.Register(value);
        remove => this._gameShuttingDown.Unregister(value);
    }

    void ErrorHandler(string name, Exception ex)
    {
         // handle event error.
    }

    public MyGame()
    {
        this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);.
    }
}

并使用以下方式调用您的异步事件:

internal async Task NotifyGameShuttingDownAsync()
{
    await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false);
}

通用版本:

public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs;

public class AsyncEvent<T> where T : AsyncEventArgs
{
    private string name;
    private List<AsyncEventHandler<T>> handlers;
    private Action<string, Exception> errorHandler;

    public AsyncEvent(string name, Action<string, Exception> errorHandler)
    {
        this.name = name;
        this.handlers = new List<AsyncEventHandler<T>>();
        this.errorHandler = errorHandler;
    }

    public void Register(AsyncEventHandler<T> handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Add(handler);
    }

    public void Unregister(AsyncEventHandler<T> handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Remove(handler);
    }

    public IReadOnlyList<AsyncEventHandler<T>> Handlers
    {
        get
        {
            var temp = default(AsyncEventHandler<T>[]);

            lock (this.handlers)
                temp = this.handlers.ToArray();

            return temp.ToList().AsReadOnly();
        }
    }

    public async Task InvokeAsync(T ev)
    {
        var exceptions = new List<Exception>();

        foreach (var handler in this.Handlers)
        {
            try
            {
                await handler(ev).ConfigureAwait(false);

                if (ev.Handled)
                    break;
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }

        if (exceptions.Any())
            this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
    }
}
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);
            }
        }
    }
}