【问题标题】:Parallel.ForEach blocking calling methodParallel.ForEach 阻塞调用方法
【发布时间】:2020-05-18 13:27:31
【问题描述】:

我遇到了Parallel.ForEach 的问题。我编写了简单的应用程序,将要下载的文件名添加到队列中,然后使用 while 循环遍历队列,一次下载一个文件,然后在下载文件后,调用另一个异步方法从下载的对象中创建对象memoryStream。该方法的返回结果不等待,直接丢弃,立即开始下一次下载。如果我在对象创建中使用简单的foreach,一切正常 - 在继续下载的同时创建对象。但是,如果我想加快对象创建过程并使用Parallel.ForEach,它将停止下载过程,直​​到创建对象。 UI 是完全响应的,但它不会下载下一个对象。我不明白为什么会发生这种情况 - Parallel.ForEach 在 await Task.Run() 内部,据我对异步编程的有限了解,这应该可以解决问题。谁能帮我理解为什么它会阻止第一种方法以及如何避免它?

这是一个小示例:

public async Task DownloadFromCloud(List<string> constructNames)
{
    _downloadDataQueue = new Queue<string>();
    var _gcsClient = StorageClient.Create();

    foreach (var item in constructNames)
    {
        _downloadDataQueue.Enqueue(item);
    }

    while (_downloadDataQueue.Count > 0)
    {
        var memoryStream = new MemoryStream();
        await _gcsClient.DownloadObjectAsync("companyprojects",
            _downloadDataQueue.Peek(), memoryStream);
        memoryStream.Position = 0;
        _ = ReadFileXml(memoryStream);
        _downloadDataQueue.Dequeue();
    }
}

private async Task ReadFileXml(MemoryStream memoryStream)
{
    var reader = new XmlReader();
    var properties = reader.ReadXmlTest(memoryStream);
    await Task.Run(() =>
    {
        var entityList = new List<Entity>();
        foreach (var item in properties)
        {
            entityList.Add(CreateObjectsFromDownloadedProperties(item));
        }
        //Parallel.ForEach(properties item =>
        //{
        //    entityList.Add(CreateObjectsFromDownloadedProperties(item));
        //});
    });
}

编辑
这是简化的对象创建方法:

public Entity CreateObjectsFromDownloadedProperties(RebarProperties properties)
{
    var path = new LinearPath(properties.Path);
    var section = new Region(properties.Region);
    var sweep = section.SweepAsMesh(path, 1);
    return sweep;
}

【问题讨论】:

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


【解决方案1】:

该方法的返回结果不等待,直接丢弃,立即开始下一次下载。

这也很危险。 “一劳永逸”的意思是“我不在乎此操作何时完成,或者它是否完成。只需丢弃所有异常,因为我不在乎。”因此,一劳永逸应该在实践中极为罕见。这里不合适。

UI 完全响应,但它不会下载下一个对象。

我不知道为什么它会阻止下载,但是切换到Parallel.ForEach 肯定会出现问题:List&lt;T&gt;.Add 不是线程安全的。

private async Task ReadFileXml(MemoryStream memoryStream)
{
    var reader = new XmlReader();
    var properties = reader.ReadXmlTest(memoryStream);
    await Task.Run(() =>
    {
        var entityList = new List<Entity>();
        Parallel.ForEach(properties, item =>
        {
            var itemToAdd = CreateObjectsFromDownloadedProperties(item);
            lock (entityList) { entityList.Add(itemToAdd); }
        });
    });
}

一个提示:如果你有一个结果值,PLINQ 通常比Parallel 更干净:

private async Task ReadFileXml(MemoryStream memoryStream)
{
    var reader = new XmlReader();
    var properties = reader.ReadXmlTest(memoryStream);
    await Task.Run(() =>
    {
        var entityList = proeprties
            .AsParallel()
            .Select(CreateObjectsFromDownloadedProperties)
            .ToList();
    });
}

但是,代码仍然存在即发即弃的问题。

为了更好地解决问题,我建议退后一步,使用更适合“管道”式处理的东西。例如,TPL 数据流:

public async Task DownloadFromCloud(List<string> constructNames)
{
  // Set up the pipeline.
  var gcsClient = StorageClient.Create();
  var downloadBlock = new TransformBlock<string, MemoryStream>(async constructName =>
  {
    var memoryStream = new MemoryStream();
    await gcsClient.DownloadObjectAsync("companyprojects", constructName, memoryStream);
    memoryStream.Position = 0;
    return memoryStream;
  });
  var processBlock = new TransformBlock<MemoryStream, List<Entity>>(memoryStream =>
  {
    var reader = new XmlReader();
    var properties = reader.ReadXmlTest(memoryStream);
    return proeprties
        .AsParallel()
        .Select(CreateObjectsFromDownloadedProperties)
        .ToList();
  });
  var resultsBlock = new ActionBlock<List<Entity>>(entities => { /* TODO */ });
  downloadBlock.LinkTo(processBlock, new DataflowLinkOptions { PropagateCompletion = true });
  processBlock.LinkTo(resultsBlock, new DataflowLinkOptions { PropagateCompletion = true });

  // Push data into the pipeline.
  foreach (var constructName in constructNames)
    await downloadBlock.SendAsync(constructName);
  downlodBlock.Complete();

  // Wait for pipeline to complete.
  await resultsBlock.Completion;
}

【讨论】:

  • 感谢您的回答!我已经对其进行了测试,似乎每个processBlock 仅在前一个processBlock 完成时才被触发。我发现我可以使用ExecutionDataflowBlockOptions 来设置块可以同时处理多少消息,但是如果我使用了例如MaxDegreeOfParallelism = 10,则必须处理的项目数量越多,总时间就越长需要到达Completion,这很有趣,因为我认为并行操作越多,我的代码运行得越好/越快。您能分享一下您对此的看法吗?
  • 可能是因为它已经是并行的:processBlock 正在执行 AsParallel inside 块。您可能不想两者都做;要么选择块内的并行处理(即我的答案中的代码),要么并行化块本身(即删除.AsParallel()并添加MaxDegreeOfParallelism)。如果每个文件都有很多对象并且您想一次处理一个文件,您希望在块内进行并行处理;如果文件可以有很少的对象并且你想一次处理多个文件,你想并行化块。
  • 有几种方法。如果您想对块进行故障排除,请让异常转义块 lambda。它将出现故障,故障将向下流向Completion。另一种方法是“铁路编程”,其中每个块实际上与 (T, Exception) 对一起工作,以便异常像常规数据一样流经管道。如果您对铁路方法感兴趣,我有一个为此设计的 Try 库。
  • 不,故障块被认为是“灾难性的”。它拆除当前块并且(因为我们正在传播完成)还将故障传播到下游的链接块。当一个块出错时,它会丢弃所有的缓冲区并拒绝新的输入。
  • 我明白了。谢谢你的回答!你帮了大忙!尽管您的回答没有直接回答我的问题,但我已经根据您的建议完全更改了我的代码,我对它的工作方式非常满意!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-04
  • 1970-01-01
  • 2021-03-01
相关资源
最近更新 更多