为什么Task.WhenAll的continuations是同步执行的?

The*_*ias 18 c# continuations task-parallel-library async-await .net-core

Task.WhenAll在 .NET Core 3.0 上运行时,我只是对该方法进行了一个奇怪的观察。我将一个简单的Task.Delay任务作为单个参数传递给Task.WhenAll,并且我预计包装的任务将与原始任务的行为相同。但这种情况并非如此。原始任务的延续是异步执行的(这是可取的),多个Task.WhenAll(task)包装器的延续是一个接一个同步执行的(这是不可取的)。

这是此行为的演示。四个工作任务正在等待同一个Task.Delay任务完成,然后继续进行繁重的计算(由 a 模拟Thread.Sleep)。

var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Run Code Online (Sandbox Code Playgroud)

这是输出。四个延续在不同的线程(并行)中按预期运行。

var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Run Code Online (Sandbox Code Playgroud)

现在,如果我注释该行await task并取消注释以下行await Task.WhenAll(task),则输出完全不同。所有延续都在同一个线程中运行,因此计算不是并行化的。每个计算都在前一个计算完成后开始:

05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
Run Code Online (Sandbox Code Playgroud)

令人惊讶的是,只有当每个工人等待不同的包装器时才会发生这种情况。如果我预先定义包装器:

var task = Task.WhenAll(Task.Delay(500));
Run Code Online (Sandbox Code Playgroud)

...然后await在所有工人中执行相同的任务,行为与第一种情况(异步延续)相同。

我的问题是:为什么会这样?是什么导致同一任务的不同包装器的延续在同一线程中同步执行?

注意:使用Task.WhenAny而不是包装任务会Task.WhenAll导致相同的奇怪行为。

另一个观察结果:我预计将包装器包装在 a 中Task.Run会使延续异步。但它没有发生。下面这行的延续仍然在同一个线程中(同步)执行。

await Task.Run(async () => await Task.WhenAll(task));
Run Code Online (Sandbox Code Playgroud)

说明:上述差异是在 .NET Core 3.0 平台上运行的控制台应用程序中观察到的。在 .NET Framework 4.8 上,等待原始任务或任务包装器之间没有区别。在这两种情况下,continuation 都是在同一个线程中同步执行的。

Jer*_*man 4

因此,您有多个异步方法等待同一个任务变量;

    await task;
    // CPU heavy operation
Run Code Online (Sandbox Code Playgroud)

是的,这些延续在完成后将被系列调用task。在您的示例中,每个延续都会在下一秒占用线程。

如果您希望每个延续异步运行,您可能需要类似的东西;

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation
Run Code Online (Sandbox Code Playgroud)

以便您的任务从初始延续返回,并允许 CPU 负载在SynchronizationContext.