具有有状态事件args的异步事件处理程序

时间:2016-02-10 23:16:09

标签: c# events asynchronous

有时我们希望事件发布者的触发操作依赖于事件处理程序设置的某些状态。一个常见示例是采用CancelEventArgs的事件。事件发布者在触发后采取行动,取决于Cancel属性的值。如果处理程序长时间运行,我们不想阻止UI线程(用户可以在等待时做其他事情)。但异步处理程序可能会在设置状态之前返回,并且发布者在需要时将没有正确的值。我通过在event参数中提供一个可变的Task属性来解决这个问题。处理程序负责设置其值,并且发布者在触发后等待它。这似乎工作正常。

一个反对意见可能是有状态事件args可以说是不好的做法,除非你只假设一个处理程序。您可以使用高阶函数而不是事件,并通过强制执行一个"处理程序来处理上述异议。

我不确定的一件事是async / await如何处理多个等待者。

  • 是否有任何关于延续执行顺序的保证?
  • 是否存在竞争条件,其余的触发可能在事件处理程序的其余部分之前执行?

答案是yes。一般来说,只要调用者和被调用者在await之后都有动作,这就会成为嵌套异步方法的问题。

  • 有什么关于我做这件事的方式有什么缺点我还没有提到过吗?
  • 是否有一些其他众所周知或广为接受的最佳实践来实现我不知道的目标?
  • 关于如何应对竞争条件的任何想法?

谢谢!

class Publisher
{
  void RaiseMyEvent()
  {
    var e = new MyEventArgs();
    OnRaiseMyEvent(e);
    if (e.Task != null) await e.HandlerTask;
    if (e.Cancel) 
    {
      // Do one thing
    }
    else 
    {
      // Do the other
    }
  }
}

class Subscriber 
{
  void MyEventHandler(object sender, CancelEventArgs e)
  {
    // Notify user to wait on process
    e.Task = SomeAsyncMethod();
    await e.Task;
    e.Cancel = GetOutcome();
    // Clear any notification
  }

  bool GetOutcome() { }
}          

实际上,我们可以通过确保在处理程序中继续之前设置触发方法所需的事件args中所需的任何状态值来避免竞争:

class Subscriber 
{
  void MyEventHandler(object sender, CancelEventArgs e)
  {
    // Notify user to wait on process
    e.Task = Task.Run(() =>
    {
      //Do stuff
      e.Cancel = GetOutcome();
    }      
    await e.Task;
    // Clear any notification
  }

  bool GetOutcome() { }
}          

两个延续都在UI线程上执行,但我们并不关心订单。

1 个答案:

答案 0 :(得分:2)

  

有时我们希望事件发布者的触发操作取决于事件处理程序设置的某些状态。

我称之为"命令事件",而不是"通知事件",并且涵盖a few approaches to async command events on my blog

经过相当多的经验后,我得出的结论是,命令事件是一种反模式。 .NET中的事件被设计为通知事件,并且使它们的行为不同是最好的尴尬。要使用GoF术语,.NET事件用于实现 Observer 设计模式,但这些"命令事件"实际上是模板方法设计模式的实现。

考虑关于模板方法设计模式的引用(第328页):

  

模板方法指定哪些操作是挂钩(可能被覆盖)以及哪些是抽象操作(必须被覆盖)

>

这是一个很好的识别命令事件的质量!如果您发现自己正在编写需要处理程序或不能拥有多个处理程序的.NET事件,那么它是一个很好的指示, .NET事件可能是错误的解决方案。

如果您有模板方法的情况,那么通常某种形式的解决方案就足够了:

interface IDetails { Task ProcessAsync(); }
class Subject
{
  private IDetails _details { get; }
  public Subject(IDetails details) { _details = details; }
  async Task SomeMethodAsync()
  {
    ...
    if (_details)
      await _details.ProcessAsync();
  }
}