在出现异常情况下,Task.WhenAll是否等待所有任务完成

24

我有两个任务。我用 Task.WhenAll 运行它们。如果其中一个任务抛出异常,另一个任务是否会继续执行完成呢?


1
如果您对立即在第一个异常上失败的WhenAll实现感兴趣,这里有一个相关的问题。 - Theodor Zoulias
3个回答

12

只需运行此代码以进行测试:

private static async Task TestTaskWhenAll()
{
    try
    {
        await Task.WhenAll(
            ShortOperationAsync(),
            LongOperationAsync()
        );
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message); // Short operation exception
        Debugger.Break();
    }
}

private static async Task ShortOperationAsync()
{
    await Task.Delay(1000);
    throw new InvalidTimeZoneException("Short operation exception");

}

private static async Task LongOperationAsync()
{
    await Task.Delay(5000);
    throw new ArgumentException("Long operation exception");
}

调试器将在5秒钟后停止。两个异常都被抛出,但是Debugger.Break()只被触发了一次。更重要的是,exception值不是AggregateException,而是InvalidTimeZoneException。这是由于新的async/await会展开为实际异常。您可以在此处阅读更多内容。如果您想阅读其他Exceptions(不仅仅是第一个),您需要从WhenAll方法调用返回的Task中读取它们。


Task.Run 对于已经是异步方法的目的是什么? - Fabio
如果你真的想阻塞一个线程 1000 毫秒,Thread.Sleep(1000)Task.Delay(1000).Wait() 更简单。 - Theodor Zoulias

11

另一个任务会完成吗?

如果另一个任务失败了,它不会被停止

但是它会完成吗?

Task.When 会等待所有任务完成,无论是否有任务失败。我刚刚进行了测试以验证 - 它花费了5秒钟才完成:

Task allTasks = Task.WhenAll(getClientToken, getVault, Task.Delay(5000)); 

如果您想将任务分组,可以创建一个“新任务”,然后等待它完成。
Task allTasks = Task.WhenAll(getClientToken, getVault, Task.Delay(5000)); 

try 
{
    await allTasks;

} catch (Exception ex) 
{

   // ex is the 'unwrapped' actual exception
   // I'm not actually sure if it's the first task to fail, or the first in the list that failed

   // Handle all if needed
   Exceptions[] allExceptions = allTasks.Exceptions;

   // OR
   // just get the result from the task / exception
   if (getVault.Status == TaskStatus.Faulted) 
   {
       ...
   }
}

5

我有同样的问题并进行了测试,简而言之:

  • 它始终等待所有任务完成。

  • 如果在所有任务完成后还有异常,将抛出第一个异常(如果不捕获将导致崩溃)。

  • 对于 所有 异常,请保留由 Task.WhenAll 返回的 Task 实例,并使用 Exception.InnerExceptions 属性。

这是我的测试:

    static async Task Main(string[] args)
    {
        var tasks = new[] { Foo1(), Foo2(), Foo3() };

        Task t = null;
        try
        {
            t = Task.WhenAll(tasks);
            await t;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }

        Console.WriteLine("All have run.");

        if (t.Exception != null) 
        {
            foreach (var ex in t.Exception.InnerExceptions)
            {
                Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            }
        }

    }

    static async Task Foo1()
    {
        await Task.Delay(50);
        throw new ArgumentException("zzz");
    }

    static async Task Foo2()
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo 2");
        throw new FieldAccessException("xxx");
    }

    static async Task Foo3()
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(200);
            Console.WriteLine("Foo 3");
        }
    }

输出:

Foo 3
Foo 3
Foo 3
Foo 3
Foo 2
Foo 3
Foo 3
Foo 3
Foo 3
Foo 3
Foo 3
ArgumentException: zzz
All have run.
ArgumentException: zzz
FieldAccessException: xxx

1
使用 Task t = null; 初始化任务容易出错。使用 Task t; 更安全,因为它强制你在 try 块内初始化变量,否则你的代码将无法编译。还要注意,在大多数情况下,t.Exception 将为 null(因为希望任务大部分时间都能成功完成)。因此,你当前的代码将在成功的情况下导致 NullReferenceException - Theodor Zoulias
在我的情况下,如果我使用Task t;,它将无法编译,因为t的赋值在try块内部。是的,我应该为t.Exception添加一个空值检查,谢谢! - Luke Vo
你说得对,我的先前建议无法编译。在进入“try”块之前初始化变量怎么样?Task t = Task.WhenAll(tasks);否则,你也应该检查t == null的情况以确保100%的安全性,这很容易被忽略。 - Theodor Zoulias
观察很好,但这个样例并不完全正确;以一种当迭代时它们会被运行/调度的方式构造任务--即使用IEnumerable懒加载。你的代码当前通过调用在定义任务数组时运行/调度了3个任务,然后--使用Task.WhenAll--确保那些在执行流中正在运行的任务完成。相反,让Task.WhenAll执行可运行任务; var runnableTasks = new [] { Foo1, Foo2, Foo3}.Select(async (fn) => await fn()),然后var t = Task.WhenAll(runnableTasks); - Brett Caswell
如果在 var tasks = new[] { Foo1(), Foo2(), Foo3() }; 后面添加 await Task.Delay(10000);,我相信你会发现它们正在运行。 - Brett Caswell
屏幕截图:https://imgur.com/a/3a1ps0L,显示任务正在运行;话虽如此,你的观察仍然是正确的,但是关于`Task.WhenAll`是否可以**永远**处理故障任务(没有ContinueWith)的行为可能取决于IEnumerable任务是“加载”还是在使用之前运行/等待/调度。当然,在调用时运行`Foo1()`可能会导致异常在你的异常处理块之外引发(如果在try-catch之前`await Task.Delay`)。 - Brett Caswell

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