等待执行事件处理程序

3

我有一个数据仓库,为我的应用程序的模型提供持久层。所有的磁盘访问都是异步的,所以我的整个持久层都是用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中等待事件处理程序的执行。这样,我就可以确保记录器在下一个对象从存储中删除之前完成其工作。

有人能指点我如何实现吗?我考虑过使用信号量,但这将打破我在数据存储库和日志记录器之间的松耦合关系。
2个回答

4
我有一篇关于“异步事件”主题的博客文章(链接)。总的来说,我建议使用“延迟”(deferrals),这是 Windows Store API 中的一个概念。
例如,使用我的 AsyncEx 库中的 DeferralManager 类型,您可以首先使您的事件参数类型支持 deferrals:
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();
}

消费代码可以使用延迟(deferral),如果需要使用await:
async void DataChangedHandler(object sender, Journaling.DataChangeEventArgs args)
{
  using (args.GetDeferral())
  {
    // Code can safely await in here.
  }
}

考虑到这个模型,对于事件处理程序来说,似乎很容易忘记需要加上 using,甚至如果他们知道,有时也会忘记。将处理程序的委托返回一个 Task 会使任何添加处理程序的人更难以正确地指示完成。他们可能会这样做,但他们需要更多或更少地试图自毁,这就不那么令人担忧了。这确实给触发事件的代码增加了一些工作量,但我认为这种权衡是值得的。 - Servy
我认为两种选择都是可行的。Task-returning事件签名的缺点是它强制所有处理程序具有异步签名,并且它不向后兼容。开发人员可能会忘记延迟,但这是WinStore API中的常见模式,因此许多开发人员对其很熟悉。 - Stephen Cleary
如果您对此有疑虑,那么您可以像为需要支持同步和异步版本的任何其他成员所做的那样:提供两者。提供同步事件和异步事件。 - Servy
我选择了你的延迟加载方法,它非常顺畅!谢谢,Stephen! - SebastianR

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);

}

谢谢你的回答。我选择了Stephen的方法,因为它更容易不改变事件的签名。 - SebastianR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接