如何正确使用 Task.WhenAll()

12

我尝试使用 Task.WhenAll 来等待多个任务的完成。

以下是我的代码 - 它应该启动多个异步任务,每个任务都会获取一个公交路线并将它们添加到本地数组中。然而,Task.WhenAll(...) 立即返回,而本地路线数组的计数为零。这似乎很奇怪,因为我期望在每个 Task 中的各种 await 语句意味着流程被挂起,并且 Task 不会返回直到它完成为止。

List<Task> monitoredTasks = new List<Task>();
foreach (BusRouteIdentifier bri in stop.services)
{
    BusRouteRequest req = new BusRouteRequest(bri.id);

    // Start a new task to fetch the route for each stop
    Task getRouteTask = Task.Factory.StartNew(async () =>
    {
        var route = await BusDataProviderManager.DataProvider.DataBroker.getRoute(req);

            // Add the route to our array (on UI thread as it's observed)
            await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, delegate
            {
                this.routes.Add(route);
            });
    });

    // Store the task in our monitoring list
    monitoredTasks .Add(getRouteTask);
}

Debug.WriteLine("Awaiting WHENALL");
await Task.WhenAll(monitoredTasks );
Debug.WriteLine(string.Format("WHENALL returned (routes count is {0} ", this.routes.Count));

this.OnWillEndFetchingRoutes(new EventArgs());

显然我做错了什么-但是是什么呢?


你有没有尝试检查最终的任务是否处于故障状态? - ie.
我认为在foreach循环中await dispatcher可能出了问题。UI线程会立即观察和显示吗? - cat916
是的,它的状态是RanToCompletion。数组中所有任务的状态也都是RanToCompletion,尽管当我逐个检查它们时,每个任务的Result字段都是WaitingForActivation。 - Carlos P
@user861114 不太确定你的意思,UI 将会观察和显示,但是 'await' 关键字不应该意味着执行直到方法完成才返回吗? - Carlos P
3个回答

9
这是由于对异步等待的实际工作原理缺乏基本理解所致。
内部任务返回流程到外部任务,然后在等待返回之前就完成了。
为了实现我想要的结果,我需要进行以下重构:
List<Task<BusRoute>> routeRetrievalTasks = new List<Task<BusRoute>>();
foreach (BusRouteIdentifier bri in stop.services)
{
    BusRouteRequest req = new BusRouteRequest(bri.id);
    routeRetrievalTasks.Add(BusDataProviderManager.DataProvider.DataBroker.getRoute(req));
}

foreach (var task in routeRetrievalTasks)
{
    var route = await task;
    this.routes.Add(route); // triggers events
}

感谢 Dave Smits 的帮助


6
我怀疑问题出在你调用 Task.Factory.StartNew() 上。我猜你最后得到的是一个Task<Task>,并且只有在实际上启动任务时才会发现这点。
请尝试使用以下代码代替:
Func<Task> taskFunc = async () =>
{
    var route = await BusDataProviderManager.DataProvider.DataBroker.getRoute(req);

    // Add the route to our array (on UI thread as it's observed)
    await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, delegate
    {
        this.routes.Add(route);
    });

}

Task getRouteTask = Task.Run(taskFunc);

谢谢。我假设我的方法其余部分将如之前一样继续,即将任务添加到任务数组中,然后在该数组上调用Task.WhenAll(...)。那么,这种方法与我回答中的方法相比如何?有什么优点吗? - Carlos P
@CarlosP:说实话,我没有足够的上下文来评论哪种方法更好。但是,是的,你的方法的其余部分将像以前一样继续进行。 - Jon Skeet

-2

那么“如何正确使用WhenAll()?”的答案实际上是:“不要使用,而是一个接一个地等待您的任务”? :D

您的解决方案可以工作,但从优化的角度来看,它是次优的。 您正在执行所有进程,等待第一个,然后继续循环。其他进程在同一时间内继续进行,其中大部分可能已经结束,但您将继续循环等待,而不是同时等待所有进程。

尽可能晚地等待任务:异步和等待

List<Task> monitoredTasks = new List<Task>();
        foreach (BusRouteIdentifier bri in stop.services)
        {
            BusRouteRequest req = new BusRouteRequest(bri.id);

            // Start a new task to fetch the route for each stop
            var route = await BusDataProviderManager.DataProvider.DataBroker.getRoute(req);

            // Add the route to our array (on UI thread as it's observed) 
            //BUT no point using a factory here, it would only create a Task<Task> while dispatcher.RunAsync is already returning you the Task you need
            // Do not await if you don't need to!!!
            Task getRouteTask = dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, delegate
            {
                this.routes.Add(route);
            });

            // Store the task in our monitoring list
            monitoredTasks.Add(getRouteTask);
        }

        Debug.WriteLine("Awaiting WHENALL");
        // And now Task.WhenAll is apply directly on your tasks and not on a list of Task<Task> returning your tasks 
        await Task.WhenAll(monitoredTasks);
        Debug.WriteLine(string.Format("WHENALL returned (routes count is {0} ", this.routes.Count));

        this.OnWillEndFetchingRoutes(new EventArgs());

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