【问题标题】:Awaiting the execution of event handlers等待事件处理程序的执行
【发布时间】:2015-04-29 08:30:32
【问题描述】:

我有一个为我的应用程序模型提供持久层的数据存储库。所有对磁盘的访问都是异步的,所以我的整个持久层都是用 async/await 编写的。我的数据存储库允许其他模块订阅数据中的更改:

 public event EventHandler<Journaling.DataChangeEventArgs> DataChanged;   
 protected void OnDataChanged(Journaling.Action a)
 {
      if (DataChanged != null)
      {
           DataChanged(this, new Journaling.DataChangeEventArgs(a));
      }
 }

当我告诉存储库删除一个被其他对象引用的对象时,它也会删除这些其他对象。

 public async Task<bool> DeleteAsync(Models.BaseModel model)
 {            
      await DeleteDependentModelsAsync(model).ConfigureAwait(false);
      if (await connection.DeleteAsync(model).ConfigureAwait(false) != 1)
          return false;
      else
      {
          var deleteAction = new Journaling.DeleteAction(model);
          OnDataChanged(deleteAction);
          return true;
      }
 }

此设置在某种程度上有效,但在删除引用其他对象的对象时出现问题。考虑这个例子:

Object X 
  Object A1: references X
  Object A2: references X
  Object A3: references X

我有一个记录器模块,它订阅数据存储库中的更改并将它们输出到文件中。记录器有时需要从存储库中获取额外的数据,以使其输出可读。删除对象 X 时的日志应该是:

A1 deleted (parent: X, more information contained in X)
A2 deleted (parent: X, more information contained in X)
A3 deleted (parent: X, more information contained in X)
X deleted

问题在于 OnDataChange 不等待事件处理程序的执行。因此,数据存储库会在记录器的事件处理程序被调用一次之前删除 A1-A3,然后删除 X。但是事件处理程序必须获取有关 X 的一些信息,这不再可能,因为数据存储库已经删除了 X。

不知何故,我不得不等待 OnDataChanged 中事件处理程序的执行。这样我可以确保记录器在下一个对象从存储中删除之前完成其工作。

任何人都可以提示我如何做到这一点的正确方向吗?我曾考虑使用信号量,但这会破坏我在数据存储库和记录器之间的松散耦合。

【问题讨论】:

    标签: c# events logging async-await


    【解决方案1】:

    我有一个blog post on the subject of "asynchronous events"。一般来说,我建议使用“延迟”,这是来自 Windows Store API 的概念。

    例如,使用我的 AsyncEx 库中的 DeferralManager 类型,您可以首先启用您的事件参数类型以支持延迟:

    public class DataChangeEventArgs : EventArgs
    {
      private readonly DeferralManager _deferrals;
    
      public DataChangeEventArgs(DeferralManager deferrals, Journaling.Action a)
      {
        _deferrals = deferrals;
      }
    
      public IDisposable GetDeferral()
      {
        return deferrals.GetDeferral();
      }
    }
    

    然后你像这样引发事件:

    protected Task OnDataChangedAsync(Journaling.Action a)
    {
      var handler = DataChanged;
      if (handler == null)
        return Task.FromResult<object>(null); // or TaskConstants.Completed
    
      var deferrals = new DeferralManager();
      var args = new Journaling.DataChangeEventArgs(deferrals, a);
      handler(args);
      return deferrals.SignalAndWaitAsync();
    }
    

    如果需要使用await,则消费代码可以使用延迟:

    async void DataChangedHandler(object sender, Journaling.DataChangeEventArgs args)
    {
      using (args.GetDeferral())
      {
        // Code can safely await in here.
      }
    }
    

    【讨论】:

    • 鉴于此模型,此类事件的处理者似乎非常容易不知道他们需要在那里拥有using,或者即使他们知道,偶尔也会忘记。让处理程序的委托返回 Task 使得任何添加处理程序的人都难以以某种方式无法正确指示完成。他们可以,但他们需要或多或少地试图在脚上开枪,这不那么令人担忧。这确实为触发事件的代码增加了一些工作量,但我认为这种权衡是值得的。
    • 我认为这两种选择都是可行的。任务返回事件签名的缺点是它强制所有处理程序具有异步签名,并且它不向后兼容。确实,开发人员可能会忘记延迟,但这是 WinStore API 中的常见模式,因此许多开发人员都熟悉它们。
    • 如果您对此感到担忧,那么您可以为需要同时支持同步和异步版本的任何其他成员做您所做的事情:两者都提供。有一个同步事件和一个异步事件。
    • 我已经选择了你的延期方法,它就像轻而易举!谢谢,斯蒂芬!
    【解决方案2】:

    由于您的处理程序是异步的,并且调用这些处理程序的类型需要知道它们何时完成,因此这些处理程序需要返回 Task 而不是 void。

    调用此处理程序时,您需要获取调用列表并调用每个单独的方法,而不是一次调用所有方法,因为您需要能够获取所有返回值。

    您还需要更改OnDataChanged 的签名以返回Task,以便调用者能够知道它何时完成。

    public event Func<Journaling.Action, Task> DataChanged;
    protected Task OnDataChanged(Journaling.Action a)
    {
        var handlers = DataChanged;
        if (handlers == null)
            return Task.FromResult(0);
    
        var tasks = DataChanged.GetInvocationList()
            .Cast<Func<Journaling.Action, Task>>()
            .Select(handler => handler(a));
        return Task.WhenAll(tasks);
    
    }
    

    【讨论】:

    • 感谢您的回答。然而,我选择了斯蒂芬斯的方法,因为不改变事件的签名更容易。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-29
    • 1970-01-01
    • 1970-01-01
    • 2016-08-17
    相关资源
    最近更新 更多