【问题标题】:Async event handlers with stateful event args具有状态事件参数的异步事件处理程序
【发布时间】:2016-02-10 23:16:09
【问题描述】:

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

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

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

  • 是否可以保证继续执行的顺序?
  • 是否会出现竞争条件,其中触发的其余部分可能在事件处理程序的其余部分之前执行?

答案是yes。这感觉这对于嵌套的异步方法来说通常是一个问题,只要调用者和被调用者在等待之后都有动作。

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

谢谢!

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

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

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 线程上执行,但我们不关心顺序。

【问题讨论】:

    标签: c# events asynchronous


    【解决方案1】:

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

    我将这些称为“命令事件”,而不是“通知事件”,并覆盖a few approaches to async command events on my blog

    经过相当多的经验,我得出的结论是命令事件是一种反模式。 .NET 中的事件被设计为通知事件,使它们的行为有所不同充其量是尴尬的。使用GoF 术语,.NET 事件用于实现 Observer 设计模式,但这些“命令事件”实际上是 Template Method 设计模式的实现.

    考虑一下关于模板方法设计模式的引用(第 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();
      }
    }
    

    【讨论】:

    • 知道了。通知/命令观察者/模板的区别抓住了我的直觉,它不是理想的方法。我很少需要多播,因此模板模式更适合。但是,如果我重新做一遍,我会使用函数式方法而不是接口,与其说是为了减少耦合,不如说是为了更轻的重量。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-31
    相关资源
    最近更新 更多