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

时间:2013-05-07 10:27:51

标签: c# .net winrt-xaml async-await

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

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

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

允许消费者订阅该活动:

public event AsyncEventHandler<bool> 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任务调度程序上执行代码。 / p>

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

private async Task<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),任务就会完成。

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

2 个答案:

答案 0 :(得分:8)

在这种情况下,您正确地意识到StartNew()返回Task<Task>,而您关心内部Task(虽然我不确定您为什么要等待外{{1}在开始Task)之前。

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

Task

几点注释:

  1. 使用事件处理程序这种方式非常危险,因为您关心从处理程序返回的await Task.WhenAll(evenHandlerTask, commandTask); ,但如果有更多处理程序,那么只有最后Task将被返回正常提升事件。如果你真的想这样做,你应该调用GetInvocationList(),它允许你分别调用和Task每个处理程序:

    await

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

  2. 如果您的private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { var handlers = handler.GetInvocationList(); foreach (AsyncEventHandler<bool> innerHandler in handlers) { await innerHandler(this, mustLoadPreviousRunningState); } } } 方法或lambda在其async之前只有await(并且没有return s),那么您就不会需要将其设为finally,只需直接返回async

    Task

答案 1 :(得分:4)

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

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

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

不幸的是,WinRT团队没有在WinRT的.NET框架中包含延迟基础结构。所以我写了blog post about async event handlers and how to build a deferral manager

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

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<T>派生自Task。这意味着Task<Task>会在没有任何警告的情况下转换为Task
  • StartNew不是async - 意识到它的行为与Task.Run不同。见Stephen Toub的excellent blog post on the subject

您的OnDoLoading方法将返回Task,表示 last 事件处理程序的完成情况。忽略其他事件处理程序中的任何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<Task>Stephen Toub's blog goes into the details of this kind of wrapping,但您可以这样想:“外部”任务代表异步OnDoLoading方法的 start (直到它必须以{{ 1}}),“inner”任务代表异步await方法的完成

接下来,我们OnDoLoading await的结果。这会解开“外部”任务,我们会得到一个StartNew代表Task中存储的OnDoLoading的完成情况。

evenHandlerTask

现在,您要返回return Task.WhenAll(evenHandlerTask, commandTask); ,表示TaskcommandTask都已完成。但是,您使用evenHandlerTask方法,因此实际的返回类型为async - 这是表示您想要的内部任务。我想你的意思是:

Task<Task>

这会给你一个await Task.WhenAll(evenHandlerTask, commandTask); 的返回类型,代表完整的完成。

如果你看看它的名称:

Task

this.PerformLoadingActionAsync().ContinueWith(...) 正在对原始代码中的外部 ContinueWith采取行动,当您真的希望它在内部 {{1}上行动时}}