Parallel.ForEach 和 async-await [重复]

Posted

技术标签:

【中文标题】Parallel.ForEach 和 async-await [重复]【英文标题】:Parallel.ForEach and async-await [duplicate] 【发布时间】:2014-04-17 15:35:45 【问题描述】:

我有这样的方法:

public async Task<MyResult> GetResult()

    MyResult result = new MyResult();

    foreach(var method in Methods)
    
        string json = await Process(method);

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);

    

    return result;

然后我决定使用Parallel.ForEach

public async Task<MyResult> GetResult()

    MyResult result = new MyResult();

    Parallel.ForEach(Methods, async method =>
    
        string json = await Process(method);    

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);
    );

    return result;

但现在我遇到了一个错误:

异步模块或处理程序已完成,而异步操作仍处于挂起状态。

【问题讨论】:

你在哪里得到这个错误?我假设这是一个例外,它是否发生在 GetResult 内? 你的Model实际上是一个视图模型,它实现了INotifyPropertyChanged并绑定到视图? 不,它不是视图模型,可能我必须更改名称。这只是一个简单的class 和一些道具 返回异常return result; GetResult 会影响性能吗? 【参考方案1】:

async 不适用于ForEach。特别是,您的 async lambda 正在转换为 async void 方法。有许多reasons to avoid async void(正如我在 MSDN 文章中描述的那样);其中之一是您无法轻松检测到 async lambda 何时完成。 ASP.NET 将看到您的代码在未完成 async void 方法的情况下返回并(适当地)抛出异常。

您可能想要做的是同时处理数据,而不是并行。几乎从不应该在 ASP.NET 上使用并行代码。以下是使用异步并发处理的代码:

public async Task<MyResult> GetResult()

  MyResult result = new MyResult();

  var tasks = Methods.Select(method => ProcessAsync(method)).ToArray();
  string[] json = await Task.WhenAll(tasks);

  result.Prop1 = PopulateProp1(json[0]);
  ...

  return result;

【讨论】:

为什么不应该在 ASP.NET 中使用 prallel? @DirkBoer:并行代码将显着降低 ASP.NET 的可扩展性,并干扰其线程池启发式算法。只有当您有可并行的 CPU 密集型工作要做并且确定您将只有少量并发用户时,它才有用。 如果我要处理大量的项目,这段代码会不会尝试同时启动所有项目,需要数百个线程?我想象将多线程级别限制在 CPU 内核数量附近会比尝试同时做所有事情更快,从而导致大量的任务切换开销。 @ygoe:“这段代码是否会尝试同时启动所有这些” 是的。 “需要数百个线程?” No. @jmath412:听起来像async 问题; Parallel.ForEach 不适用于 async。新的Parallel.ForEachAsync 可以,或者Task.WhenAll 也应该可以工作。【参考方案2】:

或者,您可以使用AsyncEnumerator NuGet Package 执行此操作:

using System.Collections.Async;

public async Task<MyResult> GetResult()

    MyResult result = new MyResult();

    await Methods.ParallelForEachAsync(async method =>
    
        string json = await Process(method);    

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);
    , maxDegreeOfParallelism: 10);

    return result;

ParallelForEachAsync 是一个扩展方法。

【讨论】:

【参考方案3】:

.NET 6 终于添加了Parallel.ForEachAsync,这是一种安排异步工作的方法,可以让您控制并行度:

var urlsToDownload = new [] 

    "https://dotnet.microsoft.com",
    "https://www.microsoft.com",
    "https://twitter.com/shahabfar"
;

var client = new HttpClient();

var options = new ParallelOptions  MaxDegreeOfParallelism = 2 ;
await Parallel.ForEachAsync(urlsToDownload, options, async (url, token) =>

    var targetPath = Path.Combine(Path.GetTempPath(), "http_cache", url);

    var response = await client.GetAsync(url, token);
    // The request will be canceled in case of an error in another URL.

    if (response.IsSuccessStatusCode)
    
        using var target = File.OpenWrite(targetPath);

        await response.Content.CopyToAsync(target);
    
);

【讨论】:

【参考方案4】:

啊,好吧。我想我知道现在发生了什么。 async method =&gt; 一个“async void”,即“即发即弃”(不推荐用于除事件处理程序之外的任何东西)。这意味着调用者无法知道它何时完成......因此,GetResult 在操作仍在运行时返回。尽管我的第一个答案的技术细节不正确,但这里的结果是相同的:当 ForEach 启动的操作仍在运行时,GetResult 正在返回。你唯一能做的就是在Process 上不要await(这样lambda 就不再是async)并等待Process 完成每次迭代。但是,这将使用至少一个线程池线程来执行此操作,从而稍微对池施加压力——可能会毫无意义地使用ForEach。我根本不会使用 Parallel.ForEach...

【讨论】:

以上是关于Parallel.ForEach 和 async-await [重复]的主要内容,如果未能解决你的问题,请参考以下文章

何时使用 Parallel.ForEach,何时使用 PLINQ

Parallel.ForEach 与 Task.Run 和 Task.WhenAll

Parallel.Invoke 和 Parallel.ForEach 本质上是一样的吗?

ThreadPool.QueueUserWorkItem 和 Parallel.ForEach 的区别?

.Net 中的多个 Parallel.ForEach 循环

C# - 用于服务调用的 Parallel.Foreach()