与异步方法的并行性:为什么我需要在这里执行 Task.Run() ,我可以避免它吗?

问题描述 投票:0回答:1

我的目标是提高对 C# 并发性的理解,并遇到了一个关于我的小玩具问题的问题。 让我们考虑一个异步方法

async Task<int> CountAsync(int id)
,它根据给定的 ID 对某些内容进行计数,但为此,它必须发出 Web 请求,因此是异步的。 现在我想并行计算多个不同的 ID。我认为不等待循环中的每个
CountAsync
调用就可以完成这项工作。

var countTasks = new List<Task<int>>();
for (int i = 1; i <= upUntil; i++)
{
    countTasks.Add(CountAsync(i));
}
await Task.WhenAll(countTasks);
var count = countTasks.Select(x => x.Result).Sum();

但是,它表明这与简单地调用并等待循环内的每个 Count 一样快。 为了让计数真正并行完成,我必须利用

Task.Run()
并编写如下代码:

var countTasks = new List<Task<int>>();
for (int i = 1; i <= upUntil; i++)
{
    countTasks.Add(Task.Run(async () => await CountAsync(i)));
}
await Task.WhenAll(countTasks);
var count = countTasks.Select(x => x.Result).Sum();

或者使用

AsParallel()
的替代方案来更好地控制并行度。

count = Enumerable.Range(1, upUntil)
    .AsParallel()
    .WithDegreeOfParallelism(16)
    .Select(CountAsync)
    .Sum(t => t.Result);

阅读 Stephen Cleary 的《C# 中的并发》让我意识到并行和异步方法是不同的,而且我实际上处于并行处理场景中。但是,我假设在第一个循环中不等待 CountAsync 并简单地收集异步方法的 Promises,也会导致执行加速,因为它可以卸载到不同的线程。

我显然在这里缺乏理解,想弄清楚我的第一个示例中幕后发生了什么。另外:当使用异步方法进行并行操作时,我的其他代码示例是否可行?我不喜欢使用.Result.

编辑: 根据要求,我添加了用于游乐场示例的 CountAsync。它计算随机“AB”字符串中“A”的数量。 Task.Delay 用于使其缓慢且异步,就像 @Auditive 发布的小提琴一样。我对小提琴很好奇,因为它似乎符合我最初的想法。一个可能很重要的细节: 我已经在 .NET 8 ASP.NET Core 应用程序的 GET 控制器方法中实现了调用代码。

private static async IAsyncEnumerable<string> GetObjectsSlowlyAsync(int id)
{
    var random = new Random(id);
    
    for (int i = 0; i < 1000000; i++)
    {
        if (random.Next(0, 10) == 1)
            yield return "A";
        else
            yield return "B";

        await Task.Delay(TimeSpan.FromMicroseconds(random.Next(400, 800)));
    }
}

public static async Task<int> CountAsync(int id)
{
    var count = 0;
    await foreach (var x in GetObjectsSlowlyAsync(id))
    {
        if (x == "A") count++;
    }
    return count;
}

我对两种调用 CountAsync 的方式体验到的差异几乎是十倍: enter image description here enter image description here

c# async-await parallel-processing task task-parallel-library
1个回答
0
投票
await Task.Delay(TimeSpan.FromMicroseconds(random.Next(400, 800)));

您似乎对 .NET 计时器在亚毫秒时间跨度内触发的能力抱有很高的期望。实际上,任何小于一毫秒的时间跨度都会蒸发为零:

Task task = Task.Delay(TimeSpan.FromMicroseconds(999));
Console.WriteLine(ReferenceEquals(task, Task.CompletedTask)); // True

在线演示.

代码中的

await
等待完成的
Task
,所以它相当于:

await Task.CompletedTask;

结果是你的

GetObjectsSlowlyAsync
是完全同步的。它既不慢也不异步。

这清楚地表明了为什么您需要

Task.Run
来并行化此代码。同步代码本身并不并行化。

© www.soinside.com 2019 - 2024. All rights reserved.