我的目标是提高对 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;
}
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
来并行化此代码。同步代码本身并不并行化。