【问题标题】:Parallel loop containing both async and synchronous包含异步和同步的并行循环
【发布时间】:2021-04-30 03:59:02
【问题描述】:

我有一个需要并行运行的循环,因为每次迭代都很慢且处理器密集,但我还需要调用异步方法作为循环中每次迭代的一部分。

我看到了有关如何在循环中处理异步方法的问题,但不是异步和同步的组合,这就是我所拥有的。

我的(简化的)代码如下 - 我知道由于异步操作被传递给 foreach,这将无法正常工作。

protected IDictionary<int, ReportData> GetReportData()
{
    var results = new ConcurrentDictionary<int, ReportData>();
      
    Parallel.ForEach(requestData, async data =>
    {
        // process data synchronously
        var processedData = ProcessData(data);

        // get some data async
        var reportRequest = await BuildRequestAsync(processedData);

        // synchronous building
        var report = reportRequest.BuildReport();

        results.TryAdd(data.ReportId, report);
     });

     // This needs to be populated before returning
     return results;
}

当操作必须是异步的以等待单个异步调用时,有什么方法可以并行执行操作。

将同步函数转换为异步函数并不实用。

我不想将操作拆分并有一个 Parallel.ForEach,然后是带有 WhenAll 和另一个 Parallel.ForEach 的异步调用,因为每个阶段的速度在不同迭代之间可能会有很大差异,因此拆分它会效率低下因为较快的会在继续之前等待较慢的。

我确实想知道是否可以使用 PLINQ ForAll 代替 Parallel.ForEach 但从未使用过 PLINQ 并且不确定它是否会在返回之前等待所有迭代完成,即任务是否仍在运行进程结束。

【问题讨论】:

  • 这能回答你的问题吗? Parallel.ForEach and async-await
  • Parallel.ForEach is not async-friendly,PLINQ 也不是。 AFAIK 处理混合同步-异步工作负载的理想工具是 TPL 数据流库。你可以看到一个例子here。请记住,TPL 数据流具有较小的学习曲线。如果您没有时间,您可以将ThreadPool 与大量线程打包,并同步处理所有内容。
  • 你能把异步调用转换成同步调用吗?使用 async/await 主要是为了隐藏 IO 操作的延迟,在并行运行时可能没有用。
  • @JonasH - 虽然异步在这里没有用,但在其他地方使用相同的方法并且异步在那里很有用;该方法最终调用了一个仅异步的库方法,因此更改它并不容易。

标签: c# async-await parallel-processing parallel.foreach plinq


【解决方案1】:

当操作必须是异步的以等待单个异步调用时,有什么方法可以并行执行操作。

是的,但是您需要了解Parallel 给您带来的好处,当您采用其他方法时您会失去。具体来说,Parallel会自动确定合适的线程数,并根据使用情况进行调整。

将同步函数转换为异步函数并不实用。

对于受 CPU 限制的方法,您不应该转换它们。

我不想将操作拆分并有一个 Parallel.ForEach,然后是带有 WhenAll 和另一个 Parallel.ForEach 的异步调用,因为每个阶段的速度在不同迭代之间可能会有很大差异,因此拆分它会效率低下因为较快的会在继续之前等待较慢的。

我会提出的第一个建议是查看TPL Dataflow。它允许您定义各种“管道”,以保持数据流动,同时限制每个阶段的并发性。

我确实想知道是否可以使用 PLINQ ForAll 代替 Parallel.ForEach

没有。 PLINQ 的工作方式与Parallel 非常相似。它们在 CPU 利用率方面的积极程度以及一些 API 差异存在一些差异——例如,如果你有一组结果出来,PLINQ 通常比Parallel 更干净——但在高级视图中它们'非常相似。两者都只适用于同步代码。

但是,您可以像这样使用简单的Task.RunTask.WhenAll

protected async Task<IDictionary<int, ReportData>> GetReportDataAsync()
{
  var tasks = requestData.Select(async data => Task.Run(() =>
  {
    // process data synchronously
    var processedData = ProcessData(data);

    // get some data async
    var reportRequest = await BuildRequestAsync(processedData);

    // synchronous building
    var report = reportRequest.BuildReport();

    return (Key: data.ReportId, Value: report);
  })).ToList();
  var results = await Task.WhenAll(tasks);
  return results.ToDictionary(x => x.Key, x => x.Value);
}

您可能需要应用并发限制(Parallel 会为您完成)。在异步世界中,这看起来像:

protected async Task<IDictionary<int, ReportData>> GetReportDataAsync()
{
  var throttle = new SemaphoreSlim(10);
  var tasks = requestData.Select(data => Task.Run(async () =>
  {
    await throttle.WaitAsync();
    try
    {
      // process data synchronously
      var processedData = ProcessData(data);

      // get some data async
      var reportRequest = await BuildRequestAsync(processedData);

      // synchronous building
      var report = reportRequest.BuildReport();

      return (Key: data.ReportId, Value: report);
    }
    finally
    {
      throttle.Release();
    }
  })).ToList();
  var results = await Task.WhenAll(tasks);
  return results.ToDictionary(x => x.Key, x => x.Value);
}

【讨论】:

  • 感谢@Stephen - 始终可以依靠您为异步问题提供全面的答案。带有信号量的 Task.Run() 看起来应该可以解决问题。 TPL Dataflow 可能是一个更好的解决方案,但看起来它会在未来增加复杂性/维护难度,所以这似乎是一个很好的折衷方案。我想知道为什么我自己没有想到的那些简单答案之一?
  • 我想你可能犯了一个小错误。异步应该在 Task.Run 内的 lambda 上,而不是 Select 内的那个。等待都在 Task.Run() 的 lambda
  • “Parallel 将自动确定适当的线程数并根据使用情况进行调整。” 这听起来比实际发生的要好。最近发现ParallelOptions.MaxDegreeOfParallelism的默认值是-1,意思是无限并行。换句话说,如果使用其默认选项调用Parallel.ForEach,就像在这个问题的代码示例中一样,它只会使ThreadPool 饱和,并保持饱和直到源可枚举完成。哎哟!
  • 是的,Parallel 将代码放在线程池中,并让线程池管理调整。请注意,它还使用智能分区,因此它不仅仅是为每个项目抛出一个任务。 PLINQ 我相信会尝试使用所有的 cores,这通常会更糟。例如,两个Parallel 调用可以共存;两个PLINQ 电话会干扰。
  • 通过使ThreadPool 饱和,Parallel 会干扰程序可能同时执行的所有操作。异步延续,System.Timers.Timer 处理程序,都将受到影响。恕我直言,这是一种不良行为。就个人而言,我不认为我会再次使用Parallel,而不指定合理的MaxDegreeOfParallelism。 AFAIK PLINQ 默认是Environment.ProcessorCount,这是合理的。即将到来的Parallel.ForEachAsync 可能会有相同的默认值(与 PLINQ 相同)。
猜你喜欢
  • 2017-06-29
  • 2018-10-17
  • 2019-08-31
  • 1970-01-01
  • 2021-12-12
  • 2018-09-18
  • 1970-01-01
  • 2015-04-18
  • 1970-01-01
相关资源
最近更新 更多