任务.WhenAll的行为与Task不同.使用了延迟



我偶然发现了一件我真的无法理解的事情。

示例代码

请考虑以下示例代码。

public static void Main()
{
Example(false)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();

Console.ReadLine();
}

public static async Task Example(bool pause)
{
List<int> items = Enumerable.Range(0, 10).ToList();
DateTime start = DateTime.Now;
foreach(var item in items) {
await ProcessItem(item, pause);
}
DateTime end = DateTime.Now;
Console.WriteLine("using normal foreach: " + (end - start));

var tasks = items.Select(x => ProcessItem(x, pause));
start = DateTime.Now;
await Task.WhenAll(tasks);

end = DateTime.Now;
Console.WriteLine("using Task.WhenAll " + (end - start));
}
public static async Task ProcessItem(int item, bool pause)
{
Console.WriteLine($"[{item}]: invoked at " + DateTime.Now.ToString("hh:mm:ss.fff tt"));
if (pause) {
await Task.Delay(1);
}
int x = 5;
for (int i = 0; i < 1 * 1000000; i++) {
x = await Calculate(i);
}

}
public static async Task<int> Calculate(int item)
{
return await Task.FromResult(item + 5);
}

Example方法中,我简单地调用ProcessItem方法,首先使用普通foreach,然后使用Task.WhenAll

ProcessItem取一些数字和一个指示是否应该调用await.TaskDelay(1)的标志,稍后会详细介绍。除此之外,它所做的一切都是模拟一些运行时间更长的代码+对第三个可用方法(Calculate(的调用。

结果

运行代码时的结果是

[0]: invoked at 01:19:17.417
[1]: invoked at 01:19:17.898
[2]: invoked at 01:19:18.330
[3]: invoked at 01:19:18.782
[4]: invoked at 01:19:19.118
[5]: invoked at 01:19:19.472
[6]: invoked at 01:19:19.716
[7]: invoked at 01:19:19.961
[8]: invoked at 01:19:20.179
[9]: invoked at 01:19:20.402
using normal foreach: 00:00:03.2314927
[0]: invoked at 01:19:20.639
[1]: invoked at 01:19:20.887
[2]: invoked at 01:19:21.178
[3]: invoked at 01:19:21.440
[4]: invoked at 01:19:21.670
[5]: invoked at 01:19:21.954
[6]: invoked at 01:19:22.390
[7]: invoked at 01:19:22.880
[8]: invoked at 01:19:23.218
[9]: invoked at 01:19:23.449
using Task.WhenAll 00:00:03.0749655

正常循环和Task.WhenAll的执行时间大致相同,看起来两个版本都是按顺序工作的,因为在这两种情况下,输出之间总是有一些延迟。

现在让我们让事情变得奇怪。如果我将true而不是false传递给Example,那么该方法现在调用await.TaskDelay(1),从而导致不同的执行,正如您在结果中看到的那样。

[0]: invoked at 01:22:17.047
[1]: invoked at 01:22:17.521
[2]: invoked at 01:22:17.886
[3]: invoked at 01:22:18.337
[4]: invoked at 01:22:18.735
[5]: invoked at 01:22:19.024
[6]: invoked at 01:22:19.262
[7]: invoked at 01:22:19.500
[8]: invoked at 01:22:19.731
[9]: invoked at 01:22:19.992
using normal foreach: 00:00:03.2050316
[0]: invoked at 01:22:20.240
[1]: invoked at 01:22:20.241
[2]: invoked at 01:22:20.241
[3]: invoked at 01:22:20.241
[4]: invoked at 01:22:20.242
[5]: invoked at 01:22:20.242
[6]: invoked at 01:22:20.242
[7]: invoked at 01:22:20.243
[8]: invoked at 01:22:20.243
[9]: invoked at 01:22:20.244
using Task.WhenAll 00:00:01.4674985

正如您所看到的,正常循环照常工作,但显然Task.WhenAll现在决定同时为所有项目调用ProcessItem方法,而在之前,一个项目接一个项目被处理。

问题

为什么执行await Task.Delay(1)会产生如此巨大的差异?

为什么第一个版本(没有调用await Task.Delay(1)(不同时调用所有项目的ProcessItem

我好像错过了什么。我已经用测试了代码。NET 4.5和。NET 4.7.2-相同的结果。

当您有await MyAsyncMethod()时,MyAsyncMethod返回的Task可能处于或不处于IsCompleted状态。而包含方法的其余部分的执行取决于该状态。

  • 当任务是";完成";,执行继续同步
  • 当任务尚未完成时,包含异步方法会返回一个未完成的任务。包含方法的其余部分将在该任务完成时执行

Calculate方法中,返回的是Task.FromResult,这是一个已完成的任务。任务然而,延迟直到超时结束才完成,因此您会立即得到一个未完成的Task。

因此,使用pause==false,您的方法将同步运行,并且在foreach完成时所有方法都已完成,而不为Task留下任何内容。何时允许等待。

对于pause==true,一旦达到延迟,ProcessItem方法就会返回一个不完整的任务。因此,该方法的几个调用会迅速启动(您可以看到Console.WriteLine的输出在时间上很接近(,只有在延迟到期后,其余的调用才会在Task.WhenAll中执行。

最新更新