为什么要继续执行Task.WhenAll的延续?


14

Task.WhenAll在.NET Core 3.0上运行时,我只是对该方法感到好奇。我将一个简单Task.Delay任务作为单个参数传递给Task.WhenAll,并且希望包装后的任务的行为与原始任务相同。但这种情况并非如此。原始任务的延续是异步执行的(这是理想的),多个Task.WhenAll(task)包装的延续是一个接一个地同步执行的(这是不希望的)。

这是此行为的演示。四个辅助任务正在等待同一Task.Delay任务完成,然后继续进行大量的计算(由模拟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);

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

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

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

05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await

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

var task = Task.WhenAll(Task.Delay(500));

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

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

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

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

await Task.Run(async () => await Task.WhenAll(task));

澄清:在.NET Core 3.0平台上运行的控制台应用程序中观察到了以上差异。在.NET Framework 4.8上,等待原始任务或任务包装器没有区别。在这两种情况下,连续都在同一线程中同步执行。


只是好奇,如果会发生await Task.WhenAll(new[] { task });什么?
vasily.sib

1
我认为这是由于内部短路Task.WhenAll
Michael Randall

3
LinqPad会为这两个变量提供相同的预期第二输出...您使用哪种环境来并行运行(控制台与WinForms与...,。NET与核心,...,框架版本)?
Alexei Levenkov

1
我能够在.NET Core 3.0和3.1上复制此行为,但只有在将初始名称Task.Delay从更改为1001000以便在awaited 时无法完成时,才可以复制。
Stephen Cleary

2
@BlueStrat很不错的发现!它肯定可以以某种方式关联。有趣的是,我未能在.NET Frameworks 4.6、4.6.1、4.7.1、4.7.2和4.8上重现Microsoft 代码的错误行为。每次我得到不同的线程ID,这是正确的行为。是在4.7.2上运行的小提琴。
Theodor Zoulias

Answers:


2

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

    await task;
    // CPU heavy operation

是的,task完成后将连续调用这些继续。在您的示例中,每个延续然后将线程占用下一秒。

如果您希望每个延续都异步运行,则可能需要类似的内容。

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

这样您的任务将从初始延续中返回,并允许CPU负载在之外运行SynchronizationContext


感谢Jeremy的回答。是的,Task.Yield是解决我的问题的好方法。不过,我的问题更多是关于为什么会发生这种情况,而不是有关如何强制执行所需行为的问题。
西奥多·祖利亚斯

如果您真的想知道,请在此处找到源代码。github.com/microsoft/referencesource/blob/master/mscorlib/...
杰里米·莱克曼

我希望这么简单,可以通过研究相关类的源代码来获得我的问题的答案。我需要花很长时间才能理解代码并弄清楚发生了什么!
Theodor Zoulias

关键是避免对原始任务SynchronizationContext调用ConfigureAwait(false)一次就足够了。
杰里米·莱克曼

这是一个控制台应用程序,SynchronizationContext.Current为null。但是我只是检查了一下以确保。我加入ConfigureAwait(false)了这一await行,没有什么区别。观察结果与以前相同。
Theodor Zoulias

1

使用创建任务时Task.Delay(),其创建选项设置为None而不是RunContinuationsAsychronously

这可能是.net框架和.net核心之间的重大突破。无论如何,它似乎确实可以解释您所观察到的行为。您也可以从挖掘到的源代码验证这Task.Delay()newing了一个DelayPromise它调用默认的Task构造函数留下没有指定创建选项。


感谢Tanveer的回答。因此,您推测在.NET Core RunContinuationsAsychronouslyNone,当构造新Task对象时,它已成为默认值,而不是?这将解释我的一些观察结果,但不是全部。具体来说,它不会解释等待相同Task.WhenAll包装程序和等待不同包装程序之间的区别。
Theodor Zoulias

0

在您的代码中,以下代码不在循环体内。

var task = Task.Delay(100);

因此,每次运行以下命令时,它将等待任务并在单独的线程中运行

await task;

但是,如果运行以下命令,它将检查的状态task,因此它将在一个线程中运行

await Task.WhenAll(task);

但是如果将任务创建移到旁边WhenAll,它将在单独的线程中运行每个任务。

var task = Task.Delay(100);
await Task.WhenAll(task);

感谢Seyedraouf的回答。不过,您的解释对我来说并不令人满意。返回的任务Task.WhenAll只是常规任务Task,就像原始任务一样task。两项任务都将在某个时间点完成,原始事件是由于计时器事件导致的,而复合任务则是由于原始任务完成的结果。为什么它们的延续显示不同的行为?一项任务在哪一方面与另一项不同?
Theodor Zoulias
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.