运行多个异步任务并等待它们全部完成

388

我需要在控制台应用程序中运行多个异步任务,并等待它们全部完成后再进行进一步处理。

有很多文章,但我似乎越读越糊涂。我已经阅读并理解了 Task 库的基本原理,但显然我在某个地方丢失了一个链接。

我知道可以链式执行任务,使它们在另一个任务完成后开始(这基本上是我读过的所有文章的情况),但我希望我的所有任务同时运行,并且我想知道何时它们都已完成。

像这种情况下最简单的实现方式是什么?

11个回答

590

两个回答都没有提到可等待的Task.WhenAll

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);
Task.WaitAllTask.WhenAll 的主要区别在于前者会阻塞(类似于对单个任务使用 Wait),而后者不会阻塞且可被等待,直到所有任务完成才将控制权交回给调用方。此外,异常处理也有所不同: Task.WaitAll: 如果至少有一个任务被取消或在执行过程中抛出了异常,则返回的 AggregateException 将包含 InnerExceptions 集合中的 OperationCanceledException。 Task.WhenAll: 如果任何一个提供的任务处于 faulted 状态,则返回的任务也会以 Faulted 状态结束,其中的异常将包含每个提供的任务中解包后的异常集合的聚合。 如果没有提供的任务 faulted,但至少有一个被取消,则返回的任务将以 Canceled 状态结束。 如果没有任务 faulted 且没有任务被取消,则结果任务将以 RanToCompletion 状态结束。 如果提供的数组/枚举不包含任务,则返回的任务将立即转换为 RanToCompletion 状态,然后返回给调用方。

7
当我尝试这样做时,我的任务是按顺序运行的?在 await Task.WhenAll(task1, task2); 前需要单独启动每个任务吗? - Zapnologica
6
Task.WhenAll并дёҚдјҡдёәдҪ еҗҜеҠЁд»»еҠЎпјҢдҪ йңҖиҰҒжҸҗдҫӣвҖңзғӯвҖқд»»еҠЎпјҢд№ҹе°ұжҳҜе·Із»ҸејҖе§Ӣжү§иЎҢзҡ„д»»еҠЎгҖӮ - Yuval Itzchakov
3
好的,那很有道理。那么你的例子会做什么呢?因为你还没有开始它们? - Zapnologica
3
@YuvalItzchakov非常感谢你!这很简单,但今天帮了我很多!至少值得+1000点赞 :) - Daniel Dušek
1
@Pierre 我不太明白。StartNew 和启动新任务与异步等待它们有什么关系? - Yuval Itzchakov
显示剩余8条评论

136

你可以创建许多任务,例如:

List<Task> TaskList = new List<Task>();
foreach(...)
{
   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);
}

Task.WaitAll(TaskList.ToArray());

63
我建议使用WhenAll。 - Ravi
可以使用await关键字而不是.Start()同时启动多个新线程吗? - Matt W
1
@MattW 不,当你使用await时,它会等待它完成。在这种情况下,您将无法创建多线程环境。这就是为什么所有任务都在循环结束时等待的原因。 - Virus
8
给未来的读者点踩,因为没有明确指出这是一个阻塞调用。 - JRoughan
1
请查看被接受的答案,了解为什么不要这样做。 - EL MOJO

65
你可以使用WhenAll返回一个可等待的TaskWaitAll,没有返回类型并将阻止进一步的代码执行,类似于Thread.Sleep,直到所有任务完成、取消或故障。

WhenAllWaitAll
任何已提供任务以错误状态完成将返回一个带有错误状态的任务。异常将包含从每个已提供任务中解压缩异常集合的聚合结果。将抛出一个AggregateException
未提供的任务中没有任何任务失败,但至少有一个被取消返回的任务将以TaskStatus.Canceled状态结束将抛出一个包含在其InnerExceptions集合中有一个OperationCanceledExceptionAggregateException
给出了一个空列表将抛出一个ArgumentException返回的任务将立即转换为TaskStatus.RanToCompletion状态,然后返回给调用方。
不会阻塞当前线程会阻塞当前线程

示例

var tasks = new Task[] {
    TaskOperationOne(),
    TaskOperationTwo()
};

Task.WaitAll(tasks);
// or
await Task.WhenAll(tasks);

如果您想以特定的顺序运行任务,您可以从这个答案中获取灵感。


抱歉晚到了,但是为什么每个操作都要使用await,同时又使用WaitAllWhenAll呢?Task[]初始化中的任务不应该没有await吗? - dee zg
@dee zg 你说得对。上面的await打败了它的目的。我会修改我的答案并将它们删除。 - NtFreX
1
是的,就是这样。感谢您的澄清!(如果回答好,请点赞) - dee zg

32
我见过的最好选择是下面的扩展方法:
public static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
    return Task.WhenAll(sequence.Select(action));
}

这样调用:

await sequence.ForEachAsync(item => item.SomethingAsync(blah));

或者使用异步lambda函数:

await sequence.ForEachAsync(async item => {
    var more = await GetMoreAsync(item);
    await more.FrobbleAsync();
});

我进行了重载以获取返回类型:public static Task ForEachAsync(this IEnumerable sequence, Func> action) { return Task.WhenAll(sequence.Select(action)); } - Brian Davis

13

又是一个答案……但通常情况下,我发现自己需要同时加载数据并将其放入变量中,例如:

var cats = new List<Cat>();
var dog = new Dog();

var loadDataTasks = new Task[]
{
    Task.Run(async () => cats = await LoadCatsAsync()),
    Task.Run(async () => dog = await LoadDogAsync())
};

try
{
    await Task.WhenAll(loadDataTasks);
}
catch (Exception ex)
{
    // handle exception
}

2
如果 LoadCatsAsync()LoadDogAsync() 只是数据库调用,那么它们是 IO-bound。Task.Run() 适用于 CPU-bound 工作;如果你只是在等待来自数据库服务器的响应,它会增加额外的不必要开销。对于 IO-bound 工作,Yuval 的被接受的答案是正确的方式。 - Stephen Kennedy
@StephenKennedy,您能否澄清一下什么样的开销以及它会对性能产生多大影响?谢谢! - Yehor Hromadskyi
这在评论框中进行概括是相当困难的 :) 我建议阅读Stephen Cleary的文章-他是这方面的专家。从这里开始:https://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html - Stephen Kennedy

8

您是否希望将Task链接起来,或者它们可以以并行方式调用?

对于链接
只需执行以下操作:

Task.Run(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);
Task.Factory.StartNew(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);

不要忘记在每个ContinueWith中检查之前的Task实例,因为它可能会出现故障。

对于并行方式
我遇到的最简单的方法是:Parallel.Invoke 否则可以使用Task.WaitAll,或者甚至可以使用WaitHandle进行倒计时以达到零操作(等等,还有一个新类:CountdownEvent),或者...


3
感谢您的回答,但您的建议可以再详细一些。 - Daniel Minnaar
@drminnaar,除了带有示例的MSDN链接之外,您还需要什么其他解释?您甚至没有点击这些链接,是吗? - user57508
4
我点击了链接并阅读了内容。我想获取Invoke的答案,但是关于它是否异步运行有很多限制和条件。你一直在修改你的答案。你发布的WaitAll链接非常完美,但我选择了展示同样功能的更快速和易于阅读的答案。请不要介意,你的答案仍然提供了好的替代方案。 - Daniel Minnaar
@drminnaar,我并不生气,只是好奇 :) - user57508
Parallel.Invoke工作得很好...直到我看到这个答案才知道它...谢谢! - jpmir

6
这是我使用数组 Func<> 的方法:
var tasks = new Func<Task>[]
{
   () => myAsyncWork1(),
   () => myAsyncWork2(),
   () => myAsyncWork3()
};

await Task.WhenAll(tasks.Select(task => task()).ToArray()); //Async    
Task.WaitAll(tasks.Select(task => task()).ToArray()); //Or use WaitAll for Sync

2
为什么不将其保留为任务数组呢? - Talha Talip Açıkgöz
1
如果你不小心执行@talha-talip-açıkgöz的任务,可能会在你意料之外执行它们。将其作为Func委托可以使你的意图更加清晰。 - DalSoft

0
你可能想看一下下面的链接,它解释了关于await async的最佳实践。 https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios
Use this...       Instead of this...  When wishing to do this...
await               Task.Wait  
                    Task.Result       Retrieving the result of a background task
await Task.WhenAny  Task.WaitAny      Waiting for any task to complete
await Task.WhenAll  Task.WaitAll      Waiting for all tasks to complete
await Task.Delay    Thread.Sleep      Waiting for a period of time

-1
如果您正在使用async/await pattern,您可以像这样并行运行多个任务:
public async Task DoSeveralThings()
{
    // Start all the tasks
    Task first = DoFirstThingAsync();
    Task second = DoSecondThingAsync();

    // Then wait for them to complete
    var firstResult = await first;
    var secondResult = await second;
}

这种方式存在一个风险,即在第二个任务完成之前,第一个任务失败导致任务丢失。正确的等待多个任务的方法是使用Task.WhenAll方法:await Task.WhenAll(first, second);,然后你可以逐个await它们以获取结果,因为你知道所有的任务都已成功完成了。 - Theodor Zoulias
@TheodorZoulias 泄漏fire-and-forget任务有问题吗?对于控制台应用程序,至少等待十分钟的WhenAll并没有什么好处,如果你错拼了输入文件名,你也无法得到提示。 - Fax
这取决于这个“fire-and-forget”任务的具体操作。在最好的情况下,它只是消耗掉一些本来就浪费的资源,比如网络带宽。但在最坏的情况下,它会在不被预期的时间修改应用程序的状态。想象一下,当用户点击一个按钮时,他们会收到一个错误消息,按钮会重新启用,然后“ProgressBar”会因为这个幽灵任务而继续上下移动...这种情况从未发生在任何由Microsoft提供的工具(如“Parallel”、PLINQ、TPL Dataflow等)中。所有这些API都不会在内部启动的所有操作完成之前返回。 - Theodor Zoulias
如果一个任务的失败使另一个任务的结果变得无关紧要,那么正确的做法是取消仍在运行的任务,并等待其完成。按照您的回答依次等待每个任务通常不是一个好主意。如果您决定对于您的用例泄漏fire-and-forget任务是可以接受的,那么对于second的失败也应该泄漏first。但是您的代码并不支持这一点。它的泄漏行为是不对称的。 - Theodor Zoulias

-1
我准备了一段代码,以展示如何在某些情况下使用任务。
    // method to run tasks in a parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks) {

        await Task.WhenAll(tasks);
    }
    // methode to run task one by one 
    public async Task RunMultipleTaskOneByOne(Task[] tasks)
    {
        for (int i = 0; i < tasks.Length - 1; i++)
            await tasks[i];
    }
    // method to run i task in parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks, int i)
    {
        var countTask = tasks.Length;
        var remainTasks = 0;
        do
        {
            int toTake = (countTask < i) ? countTask : i;
            var limitedTasks = tasks.Skip(remainTasks)
                                    .Take(toTake);
            remainTasks += toTake;
            await RunMultipleTaskParallel(limitedTasks.ToArray());
        } while (remainTasks < countTask);
    }

1
如何获取任务的结果?例如,如何将N个并行任务中的“行”(从datatable中)合并,并将其绑定到gridview asp.net? - PreguntonCojoneroCabrón

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