我偶然发现了一些我无法真正理解的事情。
示例代码
考虑这个示例代码。
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.Delay 在超时之前不会完成,因此您会立即得到一个未完成的任务。
因此对于 pause==false
,您的方法实际上是同步运行的,并且当 foreach 完成时所有方法都已完成,没有留下任何等待 Task.WhenAll 的东西。
使用 pause==true
时,ProcessItem
方法返回未完成的任务一旦延迟被命中。因此,此方法的多次调用会迅速启动(您会看到 Console.WriteLine 输出及时并排),只有在延迟到期后,才会执行其余的调用 - 在 Task.WhenAll
中。
关于c# - 如果使用 Task.Delay,Task.WhenAll 的行为会有所不同,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67485882/