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

Posted

技术标签:

【中文标题】运行多个异步任务并等待它们全部完成【英文标题】:Running multiple async tasks and waiting for them all to complete 【发布时间】:2014-09-20 11:28:57 【问题描述】:

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

那里有很多文章,但我似乎越读越困惑。我已经阅读并理解了任务库的基本原理,但我显然在某处缺少链接。

我知道可以链接任务,以便它们在另一个完成后开始(这几乎是我读过的所有文章的场景),但我希望我的所有任务同时运行,我希望完成后就知道了。

对于这样的场景,最简单的实现是什么?

【问题讨论】:

【参考方案1】:

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

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

await Task.WhenAll(task1, task2);

Task.WaitAllTask.WhenAll 之间的主要区别在于前者会阻塞(类似于在单个任务上使用 Wait),而后者不会也可以等待,将控制权交还给调用者,直到所有任务完成。

更何况,异常处理不同:

Task.WaitAll

至少有一个 Task 实例被取消 - 或 - 在至少一个 Task 实例的执行过程中引发了异常。如果任务被取消,则 AggregateException 在其 InnerExceptions 集合中包含 OperationCanceledException。

Task.WhenAll

如果提供的任何任务在故障状态下完成,则返回的任务也将在故障状态下完成,其异常将包含来自每个提供的任务的未包装异常集的聚合。

如果提供的任务都没有出错,但至少有一个任务被取消,则返回的任务将以 Canceled 状态结束。

如果没有任务出错并且没有任务被取消,则生成的任务将以 RanToCompletion 状态结束。 如果提供的数组/可枚举不包含任务,则返回的任务将在返回给调用者之前立即转换为 RanToCompletion 状态。

【讨论】:

当我尝试这个时,我的任务会按顺序运行吗?是否必须在await Task.WhenAll(task1, task2); 之前单独开始每项任务? @Zapnologica Task.WhenAll 不会为您启动任务。您必须为它们提供“热”,这意味着已经开始。 好的。这就说得通了。那么你的例子会做什么呢?因为你还没有启动它们? @YuvalItzchakov 非常感谢!它是如此简单,但它今天帮助了我很多!至少值 +1000 :) @Pierre 我没有关注。 StartNew 和旋转新任务与异步等待它们有什么关系?【参考方案2】:

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

List<Task> TaskList = new List<Task>();
foreach(...)

   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);


Task.WaitAll(TaskList.ToArray());

【讨论】:

我会推荐WhenAll 是否可以同时使用 await 关键字而不是 .Start() 启动多个新线程? @MattW 不,当您使用 await 时,它会等待它完成。在这种情况下,您将无法创建多线程环境。这就是所有任务都在循环结束时等待的原因。 对未来的读者投反对票,因为不清楚这是一个阻塞调用。 查看接受的答案,了解为什么不这样做。【参考方案3】:

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

WhenAll WaitAll
Any of the supplied tasks completes in a faulted state A task with the faulted state will be returned. The exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks. An AggregateException will be thrown.
None of the supplied tasks faulted but at least one of them was canceled The returned task will end in the TaskStatus.Canceled state An AggregateException will be thrown which contains an OperationCanceledException in its InnerExceptions collection
An empty list was given An ArgumentException will be thrown The returned task will immediately transition to a TaskStatus.RanToCompletion State before it's returned to the caller.
Doesn't block the current thread Blocks the current thread

示例

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

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

如果您想以特定/特定顺序运行任务,您可以从this 答案中获得灵感。

【讨论】:

抱歉来晚了,但是,为什么每次操作都有await,同时使用WaitAllWhenAllTask[]初始化中的任务不应该没有await吗? @dee zg 你是对的。上面的 await 违背了目的。我会更改我的答案并删除它们。 是的,就是这样。感谢您的澄清! (为好答案点赞)【参考方案4】:

我见过的最佳选择是以下扩展方法:

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();
);

【讨论】:

【参考方案5】:

还有另一个答案...但我通常会遇到这样的情况,即我需要同时加载数据并将其放入变量中,例如:

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

【讨论】:

如果 LoadCatsAsync()LoadDogAsync() 只是数据库调用,它们是 IO 绑定的。 Task.Run() 用于受 CPU 限制的工作;如果您所做的只是等待数据库服务器的响应,它会增加额外的不必要的开销。 Yuval 接受的答案是 IO-bound 工作的正确方式。 @StephenKennedy 您能否澄清一下什么样的开销以及它对性能的影响有多大?谢谢! 这在 cmets 框中很难总结 :) 相反,我建议阅读 Stephen Cleary 的文章 - 他是这方面的专家。从这里开始:blog.stephencleary.com/2013/10/…【参考方案6】:

您想链接Tasks,还是可以以并行方式调用它们?

用于链接 只需做类似的事情

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

并且不要忘记检查每个 ContinueWith 中的前一个 Task 实例,因为它可能有问题。

为并行方式 我遇到的最简单的方法:Parallel.Invoke 否则有 Task.WaitAll 或者你甚至可以使用 WaitHandles 倒计时到零个动作(等等,有一个新类:CountdownEvent),或者......

【讨论】:

感谢您的回答,但您的建议可能会得到更多解释。 @drminnaar 除了带有示例的 msdn 链接之外,您还需要哪些其他解释?你甚至没有点击链接,是吗? 我点击了链接,然后阅读了内容。我本来打算使用 Invoke,但有很多关于它是否异步运行的 If 和 But。你一直在编辑你的答案。您发布的 WaitAll 链接非常完美,但我选择了以更快、更容易阅读的方式展示相同功能的答案。不要冒犯,您的回答仍然为其他方法提供了很好的选择。 @drminnaar 没有冒犯这里,我只是好奇 :)【参考方案7】:

这就是我使用数组 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

【讨论】:

你为什么不把它保存为任务数组? 如果您不小心@talha-talip-açıkgöz,您会在您不希望它们执行时执行任务。以 Func 代表的身份执行此操作可以明确您的意图。【参考方案8】:

应该有一个比公认答案更简洁的解决方案。同时运行多个任务并获得结果不应需要三个步骤。

    创建任务 等待 Task.WhenAll(tasks) 获取任务结果(例如,task1.Result)

这里有一种方法可以将其缩减为两个步骤:

    public async Task<Tuple<T1, T2>> WhenAllGeneric<T1, T2>(Task<T1> task1, Task<T2> task2)
    
        await Task.WhenAll(task1, task2);
        return Tuple.Create(task1.Result, task2.Result);
    

你可以这样使用它:

var taskResults = await Task.WhenAll(DoWorkAsync(), DoMoreWorkAsync());
var DoWorkResult = taskResults.Result.Item1;
var DoMoreWorkResult = taskResults.Result.Item2;

这消除了对临时任务变量的需要。使用它的问题在于,虽然它适用于两个任务,但您需要为三个任务或任何其他数量的任务更新它。如果其中一项任务没有返回任何内容,它也不能很好地工作。确实,.Net 库应该提供可以做到这一点的东西

【讨论】:

【参考方案9】:

我准备了一段代码来向您展示如何在其中一些场景中使用该任务。

    // 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);
    

【讨论】:

如何获取Tasks的结果?例如,对于在数据表中合并“行”(来自 N 个并行任务)并将其绑定到 gridview asp.net ?【参考方案10】:

如果您使用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;

【讨论】:

如果first 任务在second 任务完成之前失败,这种方法会引入泄漏即发即弃任务的风险。 await多任务的正确方法是Task.WhenAll方法:await Task.WhenAll(first, second);。然后你可以单独await他们来得到他们的结果,因为你知道都已经成功完成了。 @TheodorZoulias 泄露即发即弃任务是否存在问题?似乎至少对于控制台应用程序而言,在 WhenAll 上等待十分钟以发现输入文件名拼写错误并没有什么好处。 这取决于这个即发即弃的任务是做什么的。在最好的情况下,它只会消耗资源,比如网络带宽,这些都是会浪费的。在最坏的情况下,它会在预期不会发生的时候修改应用程序的状态。想象一下,用户单击一个按钮,他们收到一条错误消息,该按钮被重新启用,然后ProgressBar 继续通过幽灵任务上下移动......微软提供的任何工具都不会发生这种情况(@ 987654330@、PLINQ、TPL 数据流等)。在所有内部启动的操作完成之前,所有这些 API 都不会返回。 如果一个任务的失败使得另一个任务的结果无关紧要,那么正确的做法是取消仍在运行的任务,await 它也完成了。正如您的回答所暗示的那样,按顺序等待每个任务很少是一个好主意。如果您认为泄漏即发即弃的任务对于您的用例来说是可以的,那么对称地,second 上的失败也应该泄漏first。你的代码不支持。它的泄漏行为是不对称的。

以上是关于运行多个异步任务并等待它们全部完成的主要内容,如果未能解决你的问题,请参考以下文章

异步IO和协程

C#异步编程概念和使用

AppDomain 等待异步任务防止 SerializationException

异步/等待和任务

使用任务Task 简化异步编程

具有同步设置的异步功能