是否可以将异步方法作为 c# 中事件处理程序的回调?

Posted

技术标签:

【中文标题】是否可以将异步方法作为 c# 中事件处理程序的回调?【英文标题】:Is it possible to have async methods as callbacks to eventhandlers in c#? 【发布时间】:2013-10-21 22:49:32 【问题描述】:

下面的例子说明了我的设计。有一个 while true 循环做某事并通过一个事件通知它已经对所有订阅者做了某事。我的应用程序在通知所有订阅者之前不应该继续执行,只要有人没有在回调中放置异步 void,它就可以工作。

如果有人在回调上放置了 async void 以等待某些任务,那么我的循环可以在回调完成之前继续。我可以做哪些其他设计来避免这种情况。

它的第 3 方插件注册主题并订阅事件,所以我无法控制他们是否放置了异步 void。可以理解,我无法为 EventHandler 执行任务回调,那么我有什么替代方案可以使用 .net 4.5。

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

namespace ConsoleApplication4

    public class Test
    

        public event EventHandler Event;

        public void DoneSomething()
        
            if (Event != null)
                Event(this,EventArgs.Empty);
        
    
    class Program
    
        static void Main(string[] args)
        
            var test = new Test();

            test.Event += test_Event;
            test.Event +=test_Event2;

            while(true)
            
                test.DoneSomething();
                Thread.Sleep(1000);

            
        

        private static void test_Event2(object sender, EventArgs e)
        
            Console.WriteLine("delegate 2");
        

        static async void test_Event(object sender, EventArgs e)
        
            Console.WriteLine("Del1gate 1");

            await Task.Delay(5000);

            Console.WriteLine("5000 ms later");

        
    

【问题讨论】:

下面两个答案都很好。我将标记投票最多的答案!我会标记他们两个,因为他们都对我帮助很大。 【参考方案1】:

如果有人在回调上放置了 async void 以等待某些任务,那么我的循环可以在回调完成之前继续。我可以做哪些其他设计来避免这种情况。

真的没有办法避免这种情况。即使您以某种方式“知道”订阅者不是通过async/await 实现的,您仍然无法保证调用者没有构建某种形式的异步“操作”。

例如,一个完全正常的 void 方法可以将其所有工作放入一个 Task.Run 调用中。

在通知所有订阅者之前,我的应用程序不应继续执行

您当前的版本确实遵循本合同。您正在同步通知订阅者 - 如果订阅者为了响应该通知而异步执行某些操作,那是您无法控制的。


可以理解,我无法为 EventHandler 执行任务回调,那么我在 .net 4.5 中有哪些替代方案。

请注意,这实际上是可能的。例如,您可以将上面的内容重写为:

public class Program

    public static void Main()
           
        var test = new Test();

        test.Event += test_Event;
        test.Event +=test_Event2;

        test.DoneSomethingAsync().Wait();
    


public delegate Task CustomEvent(object sender, EventArgs e);

private static Task test_Event2(object sender, EventArgs e)

    Console.WriteLine("delegate 2");
    return Task.FromResult(false);


static async Task test_Event(object sender, EventArgs e)

    Console.WriteLine("Del1gate 1");
    await Task.Delay(5000);
    Console.WriteLine("5000 ms later");


public class Test

    public event CustomEvent Event;
    public async Task DoneSomethingAsync()
    
        var handler = this.Event;
        if (handler != null)
        
              var tasks = handler.GetInvocationList().Cast<CustomEvent>().Select(s => s(this, EventArgs.Empty));
              await Task.WhenAll(tasks);                
        
    

您也可以按照svick 的建议使用事件添加/删除来重写它:

public class Test

    private List<CustomEvent> events = new List<CustomEvent>();
    public event CustomEvent Event
    
        add  lock(events) events.Add(value); 
        remove  lock(events) events.Remove(value); 
    

    public async Task DoneSomething()
    
        List<CustomEvent> handlers;
        lock(events) 
            handlers = this.events.ToList(); // Cache this
        var tasks = handlers.Select(s => s(this, EventArgs.Empty));
        await Task.WhenAll(tasks);
    

【讨论】:

我想知道自定义addremove 是否比使用GetInvocationList() 更有意义,尤其是因为这可能是完全类型安全的。 @svick 类似上面的东西? 是的,我就是这个意思。 很有趣,感谢您的想法 :) 这正是我所追求的。是否有意义,以及如何扩展这种订阅者可以添加 void 方法和任务方法。可以使用 BeginInvoke 和 GetInvocationList。我想我会尝试自己玩一点。再次感谢。 @s093294 不作为事件 - 事件需要具有代表的固定签名。您可以使用 2 个事件(每个事件一个)并让人们订阅其中任何一个,但是一旦您允许 void 返回方法,您就会遇到最初的问题。【参考方案2】:

在通知所有订阅者之前,我的应用程序不应继续执行,只要有人没有在回调中放置 async void 即可。

我在designing for async event handlers 上有一个博客条目。可以使用 Task-returning 委托或将现有的 SynchronizationContext 包装在您自己的内部(这将允许您检测并等待 async void 处理程序)。

但是,我建议您使用“延迟”,这是专门为解决 Windows 应用商店应用程序的此问题而设计的对象。我的AsyncEx library 中有一个简单的DeferralManager

您的事件参数可以这样定义GetDeferral 方法:

public class MyEventArgs : EventArgs

  private readonly DeferralManager deferrals = new DeferralManager();

  ... // Your own constructors and properties.

  public IDisposable GetDeferral()
  
    return deferrals.GetDeferral();
  

  internal Task WaitForDeferralsAsync()
  
    return deferrals.SignalAndWaitAsync();
  

您可以引发一个事件并(异步)等待所有异步处理程序完成,如下所示:

private Task RaiseMyEventAsync()

  var handler = MyEvent;
  if (handler == null)
    return Task.FromResult<object>(null); // or TaskConstants.Completed

  var args = new MyEventArgs(...);
  handler(args);
  return args.WaitForDeferralsAsync();

“延迟”模式的好处是它在 Windows 应用商店 API 中得到了广泛认可,因此很可能被最终用户识别。

【讨论】:

这个实现不是意味着异步订阅者同时执行(至少在没有同步上下文的情况下)?我不确定这总是需要的。 如果没有同步上下文/任务调度程序,那么是的,处理程序可以并发执行。我不认为这是一个问题。如果应该有一个上下文,它将由框架提供。其他解决方案(例如,Task.WhenAll)都具有相同的并发语义。

以上是关于是否可以将异步方法作为 c# 中事件处理程序的回调?的主要内容,如果未能解决你的问题,请参考以下文章

使用异步回调处理事件

深入理解Node.js基于事件驱动的回调

C#如何使用异步编程BeginInvoke/EndInvoke

如何在 C# 中使用 委托

C# 同步调用 异步调用 异步回调 多线程的作用

如何在一个类中实现异步