首页 > 解决方案 > 在 Parallel.Foreach 中调用异步方法的最佳实践/正确方法是什么?

问题描述

我正在创建一个 .net 核心应用程序,我需要在其中调用一个async方法来处理大量对象。我们已经在 a 内完成了这项工作,Parallel.ForEach因此我们可以利用并行性更快地完成工作。

服务方法是async我们无法改变的方法。我的问题是使用 TPL Parallel 时调用此方法的正确方法是什么。

这是一个简化的代码片段(我传递迭代 # 而不是用于演示目的的对象),以及我们的观察结果:

CallSendAsync方法在内部向 API 发出 HTTP 请求(使用 HttpClient)。

private void ParallelFor()
{
    Parallel.For(0, 100000, i =>
    {
        CallSendAsync(i).GetAwaiter().GetResult();
    });
}

我对上述代码的问题是,它使用了GetAwaiter使async方法同步的方法。然而,上面的实现非常快。它似乎也更有效地管理系统资源。

另一方面,我有这个:

private void ParallelForWithAsync()
{
    Parallel.For(0, 100000, async i =>
    {
        await CallSendAsync(i);
    });
}

此代码有一个async/await. 但是它变得非常慢,性能显着下降。它打开了大量的出站端口,最终 HTTP 请求出错。

第三,我也试过这个:

private void TaskWaitAll()
{
    IEnumerable<int> arr = Enumerable.Range(1, 100000);
    Task.WhenAll(arr.Select(CallSendAsync)).GetAwaiter().GetResult();
}

这也有与第二个片段相似的结果。

标签: c#async-awaittask-parallel-libraryparallel.foreach

解决方案


我需要调用异步方法来处理大量对象的 .net 核心应用程序。我们使用 Parallel.ForEach 完成了这项工作,因此我们可以利用并行性更快地完成工作。

让我在那儿阻止你。您不会“使用并行”来“使事情变得更快”。并行是并发的一种形式(一次做不止一件事),它是一种使用多个线程在多核机器上更快地处理 CPU 绑定算法的并发形式。但是,您的操作根本不受 CPU 限制;它是 I/O-bound,这表明 Parallel 是错误的技术。

如果您希望在基于 I/O 的处理过程中同时处理多个项目,则适当的解决方案是使用Task.WhenAll.

但是它变得非常慢,性能显着下降。它打开了大量的出站端口,最终 HTTP 请求出错。

是的。如果您实际上同时发出十万个 HTTP 请求,这是可以预料的。请记住,少于 16k IANA 临时端口。对于这样的大量请求,您可能希望将其限制在一个更合理的数量 - 例如,一次 20 个。Parallel.For将根据 CPU 使用率、线程池中的线程数等正确划分同步工作负载。要限制异步工作,您可以使用SemaphoreSlim

private async Task TaskWaitAll()
{
  var mutex = new SemaphoreSlim(20);
  IEnumerable<int> arr = Enumerable.Range(1, 100000);
  var tasks = arr.Select(async i =>
  {
    await mutex.WaitAsync();
    try { await CallSendAsync(i); }
    finally { mutex.Release(); }
  }).ToList();
  await Task.WhenAll(tasks);
}

推荐阅读