为什么调用等待过早地完成父任务?

我正在尝试创建一个控件来公开消费者可以订阅的DoLoading事件,以便执行加载操作。 为方便起见,应该从UI线程调用事件处理程序,允许消费者随意更新UI,但是他们也可以使用async / await来执行长时间运行的任务,而不会阻塞UI线程。

为此,我宣布了以下代表:

 public delegate Task AsyncEventHandler(object sender, TEventArgs e); 

这允许消费者订阅该活动:

 public event AsyncEventHandler DoLoading; 

这个想法是消费者会这样订阅事件(这条线在UI线程中执行):

 loader.DoLoading += async (s, e) => { for (var i = 5; i > 0; i--) { loader.Text = i.ToString(); // UI update await Task.Delay(1000); // long-running task doesn't block UI } }; 

在适当的时间点,我得到UI线程的TaskScheduler并将其存储在_uiScheduler

适当时由具有以下行的loader触发事件(这发生在随机线程中):

 this.PerformLoadingActionAsync().ContinueWith( _ => { // Other operations that must happen on UI thread }, _uiScheduler); 

请注意,此行不是从UI线程调用的,但需要在加载完成时更新UI,因此我在加载任务完成时使用ContinueWith在UI任务调度程序上执行代码。

我已经尝试了以下方法的几种变体,其中没有一种方法有效,所以这就是我所处的位置:

 private async Task PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread and await its execution Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState)); // This can be ignored for now as it completes immediately Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(evenHandlerTask, commandTask); } private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { await handler(this, mustLoadPreviousRunningState); } } 

正如你所看到的,我正在开始两个任务,并期望我之前的ContinueWith执行一个完成所有这些任务。

commandTask立即完成,因此暂时可以忽略它。 正如我所看到的, eventHandlerTask应该只完成一个事件处理程序完成,因为我正在等待调用事件处理程序的方法,我正在等待事件处理程序本身。

然而,实际发生的是,一旦执行我的事件处理程序中的行await Task.Delay(1000) ,任务就会完成。

为什么这样,我怎么能得到我期望的行为?

你正确地意识到StartNew()在这种情况下返回Task ,你关心内部Task (虽然我不确定你为什么在启动commandTask之前等待外部Task )。

但是然后你返回Task并忽略内部Task 。 你应该做的是使用await而不是return并将PerformLoadingActionAsync()的返回类型更改为Task

 await Task.WhenAll(evenHandlerTask, commandTask); 

几个笔记:

  1. 使用事件处理程序这种方式非常危险,因为您关心从处理程序返回的Task ,但如果有更多处理程序,则只有在正常引发事件时才会返回最后一个Task 。 如果你真的想这样做,你应该调用GetInvocationList() ,它允许你分别调用和await每个处理程序:

     private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { var handlers = handler.GetInvocationList(); foreach (AsyncEventHandler innerHandler in handlers) { await innerHandler(this, mustLoadPreviousRunningState); } } } 

    如果您知道永远不会有多个处理程序,则可以使用可以直接设置而不是事件的委托属性。

  2. 如果你有一个async方法或者lambda在它return之前只有await (并且没有finally s),那么你不需要使它async ,只需直接返回Task

     Task.Factory.StartNew(() => this.OnDoLoading(true)) 

首先,我建议您重新考虑“异步事件”的设计。

确实可以使用Task的返回值,但C#事件处理程序返回void更自然。 特别是,如果您有多个订阅,则从handler(this, ...)返回的Task只是其中一个事件处理程序的返回值。 要正确等待所有异步事件完成,您需要在引发事件时将Delegate.GetInvocationListTask.WhenAll一起使用。

由于您已经在WinRT平台上,我建议您使用“延期”。 这是WinRT团队为异步事件选择的解决方案,因此您的class级消费者应该熟悉它。

不幸的是,WinRT团队没有在WinRT的.NET框架中包含延迟基础结构。 所以我写了一篇关于异步事件处理程序以及如何构建延迟管理器的博客文章 。

使用延迟,您的事件提升代码将如下所示:

 private Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler == null) return; var args = new DoLoadingEventArgs(this, mustLoadPreviousRunningState); handler(args); return args.WaitForDeferralsAsync(); } private Task PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread. var eventHandlerTask = uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)).Unwrap(); Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(eventHandlerTask, commandTask); } 

所以这是我对解决方案的建议。 延迟的好处在于它支持同步和异步处理程序,这是WinRT开发人员已经熟悉的技术,并且无需额外代码即可正确处理多个订阅者。

现在,至于为什么原始代码不起作用,您可以通过仔细关注代码中的所有类型并确定每个任务所代表的内容来考虑这一点。 请记住以下要点:

  • Task派生自Task 。 这意味着Task将转换为Task而不会发出任何警告。
  • StartNew不是async Task.Run因此它的行为与Task.Run不同。 请参阅Stephen Toub 关于此主题的优秀博客文章 。

您的OnDoLoading方法将返回一个Task表示最后一个事件处理程序的完成。 忽略来自其他事件处理程序的任何Task (如上所述,您应该使用Delegate.GetInvocationList或deferrals来正确支持多个异步处理程序)。

现在让我们看一下PerformLoadingActionAsync

 Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState)); 

这个声明中有很多内容。 它在语义上等同于这个(稍微简单)的代码行:

 Task evenHandlerTask = await uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)); 

好的,所以我们将OnDoLoading排队到UI线程。 OnDoLoading的返回类型是Task ,因此StartNew的返回类型是Task 。 Stephen Toub的博客详细介绍了这种包装 ,但你可以这样想:“外部”任务代表异步OnDoLoading方法的开始 (直到它必须在await ),以及“ inner“task”表示异步OnDoLoading方法的完成

接下来,我们await StartNew的结果。 这解开了“外部”任务,我们得到一个Task ,表示存储在evenHandlerTaskOnDoLoading的完成。

 return Task.WhenAll(evenHandlerTask, commandTask); 

现在,您将返回一个Task ,表示commandTaskevenHandlerTask都已完成。 但是,您使用的是async方法,因此您的实际返回类型是Task – 它是表示您想要的内部任务。 我想你的意思是:

 await Task.WhenAll(evenHandlerTask, commandTask); 

哪个会给你一个返回类型的Task ,表示完全完成。

如果你看看它是如何被称为:

 this.PerformLoadingActionAsync().ContinueWith(...) 

当你真的希望它对内部 Task起作用时, ContinueWith正在对原始代码中的外部 Task执行操作。