简体   繁体   English

如何在 C# 中等待事件?

[英]How do I await events in C#?

I am creating a class that has a series of events, one of them being GameShuttingDown .我正在创建一个具有一系列事件的类,其中一个是GameShuttingDown When this event is fired, I need to invoke the event handler.当这个事件被触发时,我需要调用事件处理程序。 The point of this event is to notify users the game is shutting down and they need to save their data.此事件的目的是通知用户游戏正在关闭,他们需要保存数据。 The saves are awaitable, and events are not.保存是可等待的,而事件则不是。 So when the handler gets called, the game shuts down before the awaited handlers can complete.因此,当处理程序被调用时,游戏会在等待的处理程序完成之前关闭。

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());
}

Event registration活动报名

// 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);

I understand that the signature for events are void EventName and so making it async is basically fire and forget.我知道事件的签名是void EventName ,因此使其异步基本上是void EventName My engine makes heavy use of eventing to notify 3rd party developers (and multiple internal components) that events are taking place within the engine and letting them react to them.我的引擎大量使用事件来通知 3rd 方开发人员(和多个内部组件)引擎内正在发生事件并让他们对它们做出反应。

Is there a good route to go down to replace eventing with something asynchronous based that I can use?有没有好的方法可以用我可以使用的基于异步的东西替换事件? I'm not sure if I should be using BeginShutdownGame and EndShutdownGame with callbacks, but that's a pain because then only the calling source can pass a callback, and not any 3rd party stuff that plugs in to the engine, which is what I am getting with events.我不确定我是否应该将BeginShutdownGameEndShutdownGame与回调一起使用,但这很痛苦,因为只有调用源才能传递回调,而不是插入引擎的任何第三方内容,这就是我得到的与事件。 If the server calls game.ShutdownGame() , there's no way for engine plugins and or other components within the engine to pass along their callbacks, unless I wire up some kind of registration method, keeping a collection of callbacks.如果服务器调用game.ShutdownGame() ,引擎插件和/或引擎中的其他组件无法传递它们的回调,除非我连接某种注册方法,保留回调集合。

Any advice on what the preferred/recommended route to go down with this would be greatly appreciated!任何关于首选/推荐路线的建议将不胜感激! I have looked around and for the most part what I've seen is using the Begin/End approach which I don't think will satisfy what I'm wanting to do.我环顾四周,在大多数情况下,我看到的是使用开始/结束方法,我认为这不会满足我想要做的事情。

Edit编辑

Another option I'm considering is using a registration method, that takes an awaitable callback.我正在考虑的另一个选择是使用注册方法,该方法需要等待回调。 I iterate over all of the callbacks, grab their Task and await with a WhenAll .我遍历所有回调,获取它们的 Task 并使用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);
}

Personally, I think that having async event handlers may not be the best design choice, not the least of which reason being the very problem you're having.就我个人而言,我认为拥有async事件处理程序可能不是最好的设计选择,其中最重要的原因是您遇到的问题。 With synchronous handlers, it's trivial to know when they complete.使用同步处理程序,知道它们何时完成是微不足道的。

That said, if for some reason you must or at least are strongly compelled to stick with this design, you can do it in an await -friendly way.也就是说,如果由于某种原因您必须或至少被迫坚持这种设计,您可以以await友好的方式进行。

Your idea to register handlers and await them is a good one.您注册处理程序并await它们的想法是个好主意。 However, I would suggest sticking with the existing event paradigm, as that will keep the expressiveness of events in your code.但是,我建议坚持使用现有的事件范式,因为这将保持代码中事件的表现力。 The main thing is that you have to deviate from the standard EventHandler -based delegate type, and use a delegate type that returns a Task so that you can await the handlers.最重要的是,您必须偏离基于标准EventHandler的委托类型,并使用返回Task的委托类型,以便您可以await处理程序。

Here's a simple example illustrating what I mean:这是一个简单的例子来说明我的意思:

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);
    }
}

The OnShutdown() method, after doing the standard "get local copy of the event delegate instance", first invokes all of the handlers, and then awaits all of the returned Tasks (having saved them to a local array as the handlers are invoked). OnShutdown()方法在执行标准的“获取事件委托实例的本地副本”之后,首先调用所有处理程序,然后等待所有返回的Tasks (在调用处理程序时将它们保存到本地数组) .

Here's a short console program illustrating the use:这是一个简短的控制台程序,说明了用法:

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");
    }
}

Having gone through this example, I now find myself wondering if there couldn't have been a way for C# to abstract this a bit.看完这个例子后,我现在发现自己想知道 C# 是否没有办法将其抽象化。 Maybe it would have been too complicated a change, but the current mix of the old-style void -returning event handlers and the new async / await feature does seem a bit awkward.也许更改太复杂了,但是当前的旧式void返回事件处理程序和新的async / await功能的组合看起来确实有点尴尬。 The above works (and works well, IMHO), but it would have been nice to have better CLR and/or language support for the scenario (ie be able to await a multicast delegate and have the C# compiler turn that into a call to WhenAll() ).上面的工作(并且工作得很好,恕我直言),但是如果为场景提供更好的 CLR 和/或语言支持会很好(即能够等待多播委托并让 C# 编译器将其转换为对WhenAll()的调用WhenAll() )。

Peter's example is great, I've just simplified it a little using LINQ and extensions: 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;
    }
}

It may be a good idea to add a timeout.添加超时可能是个好主意。 To raise the event call Raise extension:要引发事件调用 Raise 扩展:

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

private async Task SomeMethod()
{
    ...

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

    ...
}

But you have to be aware that, unlike synchronous evens, this implementation calls handlers concurrently.但您必须注意,与同步事件不同,此实现并发调用处理程序。 It can be an issue if handlers have to be executed strictly consecutively what they are often do, eg a next handler depends on results of the previous one:如果处理程序必须严格连续执行它们经常做的事情,那么这可能是一个问题,例如下一个处理程序取决于前一个处理程序的结果:

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;
    }
}

You'd better change the extension method to call handlers consecutively:您最好将扩展方法更改为连续调用处理程序:

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);
            }
        }
    }
}
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);
            }
        }
    }
}
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);
        }
    }
}

example:例子:

    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);
                }
            });
        });
    }

Note: This is async so the event handler may compromise the UI thread.注意:这是异步的,因此事件处理程序可能会危及 UI 线程。 The event handler (subscriber) should do no UI-work.事件处理程序(订阅者)不应该做 UI 工作。 It wouldn't make much sense otherwise.否则就没多大意义了。

  1. declare your event in your event provider:在您的事件提供程序中声明您的事件:

    public event EventHandler DoSomething;公共事件 EventHandler DoSomething;

  2. Invoke event your provider:调用事件您的提供者:

    DoSomething.InvokeAsync(new MyEventArgs(), this, ar => { callback called when finished (synchronize UI when needed here!) }, null); DoSomething.InvokeAsync(new MyEventArgs(), this, ar => { 完成时调用回调(此处需要时同步 UI!) }, null);

  3. subscribe the event by client as you would normally do像往常一样由客户订阅事件

It's true, events are inherently un-awaitable so you'll have to work around it.的确,事件本质上是不可等待的,因此您必须解决它。

One solution I have used in the past is using a semaphore to wait for all entries in it to be released.我过去使用一种解决方案是使用信号量来等待其中的所有条目被释放。 In my situation I only had one subscribed event so I could hardcode it as new SemaphoreSlim(0, 1) but in your case you might want to override the getter/setter for your event and keep a counter of how many subscribers there are so you can dynamically set the max amount of simultaneous threads.在我的情况下,我只有一个订阅事件,因此我可以将其硬编码为new SemaphoreSlim(0, 1)但在您的情况下,您可能希望覆盖事件的 getter/setter 并保留有多少订阅者的计数器,以便您可以动态设置最大并发线程数。

Afterwards you pass a semaphore entry to each of the subscribers and let them do their thing until SemaphoreSlim.CurrentCount == amountOfSubscribers (aka: all spots have been freed).之后,您将信号量条目传递给每个订阅者,让他们做自己的事情,直到SemaphoreSlim.CurrentCount == amountOfSubscribers (又名:所有点都已被释放)。

This would essentially block your program until all event subscribers have finished.这实际上会阻塞您的程序,直到所有事件订阅者都完成。

You might also want to consider providing an event à la GameShutDownFinished for your subscribers, which they have to call when they're done with their end-of-game task.您可能还想考虑为您的订阅者提供一个GameShutDownFinished的事件,他们在完成游戏结束任务后必须调用该事件。 Combined with the SemaphoreSlim.Release(int) overload you can now clear up all semaphore entries and simply use Semaphore.Wait() to block the thread.结合SemaphoreSlim.Release(int)重载,您现在可以清除所有信号量条目并简单地使用Semaphore.Wait()来阻止线程。 Instead of having to check whether or not all entries have been cleared you now wait until one spot has been freed (but there should only one moment where all spots are freed at once).现在不必检查是否所有条目都已被清除,您现在可以等到一个位置被释放(但应该只有一个时刻所有位置都被一次释放)。

I know that the op was asking specifically about using async and tasks for this, but here is an alternative that means the handlers do not need to return a value.我知道操作员专门询问使用 async 和任务为此,但这里有一个替代方案,这意味着处理程序不需要返回值。 The code is based on Peter Duniho's example.该代码基于 Peter Duniho 的示例。 First the equivalent class A (squashed up a bit to fit) :-首先是等效的 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); });
    }
}

A simple console application to show its use...一个简单的控制台应用程序来展示它的用途......

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); }
}

I hope that this is useful to someone.我希望这对某人有用。

If you need to await a standard .net event handler you can't do that, because it's void .如果您需要等待标准的 .net 事件处理程序,则不能这样做,因为它是void

But you can create an async event system to handle that:但是您可以创建一个异步事件系统来处理:

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));
    }
}

And you can declare now your async events:您现在可以声明您的异步事件:

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);.
    }
}

And invoke your async event using:并使用以下方法调用您的异步事件:

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

Generic version:通用版本:

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));
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM