如果使用Task.Delay,Task.WhenAll的行为会有所不同

3
我发现了一些我无法理解的内容。
示例代码
考虑这个示例代码。
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.WhenAllProcessItem需要一些数字和一个指示是否应调用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 执行时间大致相同,看起来两个版本都是按顺序运行的,因为在两种情况下输出之间总是存在一定的延迟。

现在让我们来点奇怪的事情。如果我将 Example 中的参数从 false 改为 true,那么该方法会调用 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测试了代码,结果相同。

4
在你的代码中,Task.Delay 调用是唯一实际异步处理的地方。如果没有它,所有东西都将按顺序运行。 - Klaus Gütter
2
Task.FromResult 并不能神奇地将一个 CPU 绑定的操作变成异步。 - Charlieface
为什么var tasks = items.Select(x => ProcessItem(x, pause));start = DateTime.Now;之前? - asaf92
1个回答

5
当你有 await MyAsyncMethod() 时,它返回一个 Task,可能处于已完成或未完成状态。并且包含方法的其余部分的执行取决于该状态。
  • 当任务“完成”时,执行将同步继续。
  • 当任务尚未完成时,包含异步方法返回一个未完成的任务。只有在此任务完成时,才会执行包含方法的其余部分。

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

因此,如果 pause==false,则您的方法实际上是同步运行的,并且当 foreach 完成时,所有方法都已完成,没有留下任何内容供 Task.WhenAll 等待。

使用pause==trueProcessItem方法会在Delay时间到达时立即返回一个不完整的任务。因此,多个此方法的调用会迅速启动(您可以看到Console.WriteLine输出的时间很接近),只有在延迟过期后,才会执行其余部分-在Task.WhenAll中。


感谢您的澄清,我理解了。在我的实际情况中,当调用一些还不完全异步的外部事物时,我有某种“暂停”开关。一旦这种情况发生改变,我认为问题就会得到解决。 - Trexx

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