1

我正在尝试创建一个控件,该控件公开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
                }
            };

在适当的时间点,我得到一个TaskSchedulerUI 线程并将其存储在_uiScheduler.

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

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

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

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

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)我的事件处理程序中的行执行,任务就会完成。

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

4

2 回答 2

8

您正确地意识到在这种情况下StartNew()返回Task<Task>,并且您关心内部Task(尽管我不确定您为什么要Task在开始之前等待外部commandTask)。

但是随后您返回Task<Task>并忽略内部Task。您应该做的是使用await而不是return将返回类型更改PerformLoadingActionAsync()为 just 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<bool> innerHandler in handlers)
            {
                await innerHandler(this, mustLoadPreviousRunningState);
            }
        }
    }
    

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

  2. 如果您有一个async方法或 lambda 仅await在其之前return(并且没有finallys),那么您不需要 make 它async,只需Task直接返回:

    Task.Factory.StartNew(() => this.OnDoLoading(true))
    
于 2013-05-07T13:08:26.603 回答
4

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

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

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

不幸的是,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<T>源自Task。这意味着Task<Task>将转换为Task没有任何警告。
  • StartNew不知道async,所以它的行为不同于Task.Run. 请参阅 Stephen Toub关于该主题的出色博客文章

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

现在让我们看看PerformLoadingActionAsync

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

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

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

好的,所以我们正在排队OnDoLoading到 UI 线程。OnDoLoadingis的返回类型Task,所以 is 的返回StartNew类型Task<Task>Stephen Toub 的博客详细介绍了这种包装,但您可以这样想:“外部”任务代表异步方法的开始OnDoLoading(直到它必须在 an 处产生await),而“内部”任务task 表示异步方法的完成。OnDoLoading

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

return Task.WhenAll(evenHandlerTask, commandTask);

现在您将返回一个Task表示两者commandTaskevenHandlerTask完成的时间。但是,您在一个async方法中,因此您的实际返回类型是Task<Task>- 它是代表您想要的内部任务。我想你的意思是:

await Task.WhenAll(evenHandlerTask, commandTask);

这会给你一个返回类型Task,代表完全完成。

如果你看看它是如何被调用的:

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

ContinueWith是作用于原始代码中的外部 Task,当您真的希望它作用于内部 Task时。

于 2013-05-07T13:15:34.763 回答