为什么Task.WhenAny没有抛出预期的TimeoutException?

28

请注意下面这段简单的代码:

class Program
{
    static void Main()
    {
        var sw = new Stopwatch();
        sw.Start();
        try
        {
            Task.WhenAny(RunAsync()).GetAwaiter().GetResult();
        }
        catch (TimeoutException)
        {
            Console.WriteLine("Timed out");
        }
        Console.WriteLine("Elapsed: " + sw.Elapsed);
        Console.WriteLine("Press Enter to exit");
        Console.ReadLine();
    }

    private static async Task RunAsync()
    {
        await Observable.StartAsync(async ct =>
        {
            for (int i = 0; i < 10; ++i)
            {
                await Task.Delay(500, ct);
                Console.WriteLine("Inside " + i);
            }
            return Unit.Default;
        }).Timeout(TimeSpan.FromMilliseconds(1000));
    }
}

运行它将输出:

Inside 0
Inside 1
Elapsed: 00:00:01.1723818
Press Enter to exit

请注意,没有超时信息。

现在,如果我将Task.WhenAny替换为Task.WhenAll,会得到以下结果:

Inside 0
Inside 1
Timed out
Elapsed: 00:00:01.1362188
Press Enter to exit

注意这次出现了已超时的消息。

如果完全删除Task.WhenAll包装并直接调用RunAsync

Inside 0
Inside 1
Timed out
Elapsed: 00:00:01.1267617
Press Enter to exit

正如预期的那样,出现了超时消息。

那么Task.WhenAny是怎么回事呢?它显然会中断异步方法,但是TimeoutException在哪里呢?


1
为什么要使用.Task.GetAwaiter().GetResult(),首先它等同于Task.Result,其次你很可能会被吃掉。 - Aron
2
@Aron 这并不等同。它会抛出实际的异常。但是,是的,它仍然是阻塞的,可能会导致死锁。 - i3arnon
Task.getAwaiter()此方法旨在供编译器使用,而不是应用程序代码中使用。 - sstan
1
@sstan - 我知道这一点,但它是唯一真正的阻塞等效于await.Result.Wait()具有不同的异常处理语义。 - mark
1个回答

69

Task.WhenAny 不会重新抛出任务中的异常(与 Task.WhenAll 不同):

"返回的任务将在任何一个提供的任务完成时完成。返回的任务将始终以 RanToCompletion 状态结束,并将其 Result 设置为第一个完成的任务。即使第一个完成的任务以 CanceledFaulted 状态结束,这也是正确的。"

引自Task.WhenAny

这意味着无论如何它都会成功地完成而不带有任何类型的异常。

要实际重抛已完成任务的异常,您需要await返回的任务本身:

var completedTask = await Task.WhenAny(tasks); // no exception
await completedTask; // possible exception

或者针对你的情况:

Task.WhenAny(RunAsync()).GetAwaiter().GetResult().GetAwaiter().GetResult();

哦,它返回一个 Task<Task>。现在有意义了,但为什么它有这样奇怪的返回类型呢? - mark
4
@mark 外部任务存在是为了让你等待它。内部任务存在是为了让你得到第一个已完成任务的结果。当你使用 Task.WhenAny 时,你可能想知道哪个任务先完成,或者它的结果是什么。 - i3arnon
8
这是否意味着你可以合法地编写 await await Task.WhenAny(tasks),@i3arnon? - Mr. Boy
3
请注意,虽然您可以合法地编写await await Task.WhenAny(tasks),但是只有第一个任务的结果/状态会以这种方式进行检查。 - ChristofSenn
1
这在异常情况下很重要,以防止UnobservedTaskExceptions。 - AyCe
哦,谢谢这个。起初我还以为出了什么问题,因为没有抛出异常。 - sabiland

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