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,所以使它异步基本上是火和忘记.我的引擎大量使用事件来通知第三方开发人员(和多个内部组件)事件正在引擎内发生并让他们对它们做出反应.
是否有一个很好的途径可以使用我可以使用的异步方式来替换事件?我不确定我是否应该使用BeginShutdownGame和EndShutdownGame回调,但这是一个痛苦,因为那时只有调用源可以传递回调,而不是任何插入引擎的第三方东西,这是我得到的事件.如果服务器调用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())会很不错..
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)
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)