【问题标题】:How to invoke an event asynchronously?如何异步调用事件?
【发布时间】:2017-07-28 17:13:16
【问题描述】:

我正在构建一个可供其他开发人员使用的控件,并且在一些地方我有开发人员可以订阅的事件来执行一些可能很长的操作。

我的目标是让他们选择使用 async/await、多线程、backgroundworkers 等。如果他们选择,但仍然能够在事件调用完成之前完成执行。这是一个伪代码示例(我意识到这不会编译,但希望意图很明确):

我的代码:

public event MyEventHandler MyEvent;
private void InvokeMyEvent()
{
    var args = new MyEventArgs();

    // Keep the UI responsive until this returns
    await MyEvent?.Invoke(this, args);

    // Then show the result
    MessageBox.Show(args.Result);
}

开发者/订阅者的潜在代码:

// Option 1: The operation is very quick,
// so the dev doesn't mind doing it synchronously
private void myForm_MyEvent(object sender, MyEventArgs e)
{
    // Short operation, I'll just do it right here.
    string result = DoQuickOperation();
    e.Result = result;
}

// Option 2, The operation is long,
// so the dev wants to do it on another thread and keep the UI responsive.
private void myForm_MyEvent(object sender, MyEventArgs e)
{
    myForm.ShowProgressPanel();

    // Long operation, I want to multithread this!
    Task.Run(() =>
    {
        string result = DoVeryLongOperation();
        e.Result = result;
    })
    .ContinueWith((task) =>
    {
        myForm.HideProgressPanel();
    }
}

有没有标准的方法来完成这个?我正在查看委托的 BeginInvoke 和 EndInvoke 方法,希望它们能有所帮助,但我并没有真正想出任何东西。

任何帮助或建议将不胜感激!谢谢!

【问题讨论】:

  • 每个订阅者都会有结果吗?
  • 实际上只有一个订阅者,但如果有多个,我只关心最后一个。如果他们不设置它,结果就是 MyEventArgs 构造它的任何内容。
  • 您需要进行两项更改:(1) 处理程序需要返回Task,以便调用代码可以知道它们何时完成。 (2) 你不能使用语法糖来忽略订阅的处理程序的数量。 ?.Invoke 不仅不起作用,因为它让您在没有处理程序时尝试等待 null,多播也不起作用。 您需要调用GetInvocationList() 并循环。 即使最后一个处理程序是获得确定args.Result 值的特权的处理程序,您仍然需要等待返回的Task 对象来自其他人。
  • 1) 这不会破坏事件处理程序的约定吗?即使他们不使用任务,它也会迫使开发人员返回任务。 2)当然,有道理,但我需要他们一个接一个地执行并返回,这是无法做到的。
  • 1) 太糟糕了,您要求的东西在不违反约定的情况下是不可能的。 2)我只是告诉你怎么做,所以不要告诉我它做不到。在获得第二个之前是否等待第一个 Task 取决于您...但是如果您让异步处理程序并行运行,您将无法控制对 args.Result 的更改发生的顺序。

标签: c# multithreading events asynchronous


【解决方案1】:

没有标准的方法。

当我遇到这种情况时,我个人的偏好是使用返回 Task 的自定义委托类型。同步返回的处理程序可以简单地返回Task.CompletedTask,他们不需要为此创建新任务。

public delegate Task MyAsyncEventHandler(object sender, MyEventArgs e);
public event MyAsyncEventHandler MyEvent;
private async Task InvokeMyEvent()
{
    var args = new MyEventArgs();

    // Keep the UI responsive until this returns
    var myEvent = MyEvent;
    if (myEvent != null)
        await Task.WhenAll(Array.ConvertAll(
          myEvent.GetInvocationList(),
          e => ((MyAsyncEventHandler)e).Invoke(this, args)));

    // Then show the result
    MessageBox.Show(args.Result);
}

注意在局部变量中捕获myEvent 以避免线程问题,并使用GetInvocationList() 确保等待所有任务。

请注意,在此实现中,如果您有多个事件处理程序,它们会同时调度,因此可以并行执行。处理程序需要确保他们不会以线程不安全的方式访问args

根据您的用例,按顺序执行处理程序(await 在循环中)可能更合适。

【讨论】:

  • 线程安全不是访问args 的问题,因为async 任务之间的并发是协作切换,而不是真正的并行。但是防止重新排序和重新进入很重要。
  • @BenVoigt 线程安全是一个问题,因为后台线程可以在检查 if (MyEvent != null) 之后但在调用 MyEvent.GetInvocationList() 之前取消订阅事件处理程序。即使使用标准签名,这也是事件处理程序的一个众所周知的问题。
  • 嗯,这与访问 args 的方式完全不同,这是我打算将我的评论应用于您的声明。
  • 就此而言,调用列表也存在切换和重入问题,而不仅仅是线程。
  • @BenVoigt 因为它们都是从 UI 线程调用的,所以在该 UI 线程上运行的所有东西都是顺序的,但是任何处理程序所做的任何实际并行工作都不需要在 UI 线程上将并行发生(除非他们专门做了其他事情)。具体来说,OP 展示的实际示例将导致并行完成一些工作。
【解决方案2】:

您可能会发现我的blog post on async events 很有用。

没有严格的约定,但由于 WinRT/Win10 API 处理此问题的方式,存在一个宽松的约定:延迟。我有一个DeferralManager type 可以帮助您:

class MyEventArgs : IDeferralSource
{
  private IDeferralSource _deferralSource;
  public MyEventArgs(IDeferralSource deferralSource) { _deferralSource = deferralSource; }
  IDisposable GetDeferral() => _deferralSource.GetDeferral();

  ... // Other properties
}

public event MyEventHandler MyEvent;
private async Task InvokeMyEventAsync()
{
  var deferralManager = new DeferralManager();
  var args = new MyEventArgs(deferralManager.DeferralSource);

  MyEvent?.Invoke(this, args);

  await deferralManager.WaitForDeferralsAsync();

  MessageBox.Show(args.Result);
}

这种方法具有很好的特性,即同步事件处理程序与普通事件处理程序完全一样,同时允许异步事件处理程序。但这并不理想;特别是,强制异步事件处理程序获取延迟 - 如果没有,那么它不会按预期工作。

但是,我必须提醒您,首先将事件用于这种设计是可疑的。在 .NET 中,事件是观察者设计模式的适当实现。但是您的代码没有使用观察者;它使用模板方法设计模式。您遇到的设计问题是因为您试图使用事件来实现模板方法设计模式。您可以强制它适应(以几种不同的方式),但这并不理想。

【讨论】:

  • 您好,感谢您的想法。针对您的最后一段,您是否提出了替代路线?如果它有帮助,一个特定的情况是我正在加载一个 DataTable,就在我对其进行处理之前,我调用此事件以允许订阅者修改 DataTable,或者通过 MyEventArgs 提供他们自己的自定义.然后,我在其余代码中使用此 DataTable。
  • 您可以使用继承,其中MyEvent 变为virtual Task UpdateDataDable(DataTable x);,或者您可以使用组合,其中MyEvent 变为IDataTableUpdater { Task UpdateDataTable(DataTable x); } 的实例,该实例被传递到您的类型的构造函数中。我个人更喜欢作曲。
  • 遗憾的是,我认为这些选项中的任何一个都不适合我,因为此代码位于 Control 上……位于 Control 上……位于 Control 上。为方便起见,我在顶级控件上公开了我的事件,因为子控件是有意私有的。
  • 我不明白为什么这些方法行不通。尤其是组合一 - 私有派生类型变得笨拙。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-16
相关资源
最近更新 更多