嵌套 Parallel.ForEach 循环

问题描述 投票:0回答:4

我有一些代码,目前正在针对多核架构中的并发性进行优化。在我的一堂课中,我发现了一个嵌套的

foreach
循环。基本上,外循环遍历一组
NetworkInterface
对象。内部循环迭代网络接口 IP 地址。

这让我思考,嵌套

Parallel.ForEach
循环一定是个好主意吗?阅读本文后(同一列表中的嵌套 Parallel.ForEach 循环?)我仍然不确定在效率和并行设计方面什么适用于何处。此示例将
Parallel.Foreach
语句应用于列表,其中两个循环都对该列表执行操作。

在我的示例中,循环正在做不同的事情,所以我应该:

  1. 使用嵌套
    Parallel.ForEach
    循环?
  2. 在父循环上使用
    Parallel.ForEach
    并保持内部循环不变?
c# performance concurrency task-parallel-library parallel.foreach
4个回答
34
投票

Parallel.ForEach 不一定并行执行——它只是一个请求,如果可能的话,这样做。因此,如果执行环境没有CPU能力来并行执行循环,它就不会这样做。

如果循环上的操作不相关(即,如果它们是独立的并且不相互影响),则我认为在内循环和外循环上使用 Parallel.ForEach 没有问题。

这确实取决于执行环境。如果您的测试环境与生产环境足够相似,您可以进行时序测试,然后确定要做什么。如有疑问,请进行测试;-)

祝你好运!


3
投票

答案是,这取决于;

  1. 获得 IP 地址后,您将如何处理它?
  2. 每一步需要多长时间?

线程并不便宜,它们需要时间来创建,并且需要内存来存在。 如果您没有对这些 IP 地址执行计算成本较高的操作,并且使用错误的集合类型进行并发访问,那么您几乎肯定会减慢应用程序的速度。

使用

StopWatch
来帮助您回答这些问题。


1
投票

我的建议是遵循第二种方法:仅并行化外循环,并保持内循环顺序(

for
/
foreach
)。不要将
Parallel.ForEach
将一个循环放在另一个内部。原因是:

  1. 并行化增加了开销。每个

    Parallel
    循环必须同步
    source
    的枚举、启动
    Task
    、监视取消/终止标志等。通过嵌套
    Parallel
    循环,您将多次支付此成本。

  2. 限制并行度变得更加困难。

    MaxDegreeOfParallelism
    选项不是影响子循环的环境属性。它仅限制单个循环。因此,如果您有一个带有
    Parallel
    的外部
    MaxDegreeOfParallelism = 4
    循环和一个也带有
    Parallel
    的内部
    MaxDegreeOfParallelism = 4
    循环,则内部
    body
    可能会同时调用 16 次 (
    4 * 4
    )。仍然可以通过使用相同的
    TaskScheduler
    配置所有循环来强制实施合理的上限,特别是使用共享
    ConcurrentScheduler
    实例的
    ConcurrentExclusiveSchedulerPair
    属性。

  3. 如果出现异常,您将得到一个深层嵌套的

    AggregateException
    ,您必须
    Flatten

我还建议考虑第三种方法:在扁平源序列上执行单个

Parallel
循环。例如代替:

ParallelOptions options = new() { MaxDegreeOfParallelism = X };

Parallel.ForEach(NetworkInterface.GetAllNetworkInterfaces(), options, ni =>
{
    foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
    {           
        // Do stuff with ni and ip
    });
});

...你可以这样做:

var query = NetworkInterface.GetAllNetworkInterfaces()
    .SelectMany(ni => ni.GetIPProperties().UnicastAddresses, (ni, ip) => (ni, ip));

Parallel.ForEach(query, options, pair =>
{
    (ni, ip) = pair;
    // Do stuff with ni and ip
});

此方法仅并行化

Do stuff
ni.GetIPProperties()
的调用不是并行的。 IP 地址按顺序获取,一次获取一个
NetworkInterface
。它还强化了每个
NetworkInterface
的并行化,这可能不是您想要的(您可能希望将并行化分散到许多
NetworkInterface
之间)。因此,这种方法的特点使其对于某些场景很有吸引力,但不适合其他场景。

还有一个值得一提的情况是,外层序列和内层序列中的对象属于同一类型,并且具有父子关系。在这种情况下,请查看这个问题:C# 中的并行树遍历


0
投票

我刚刚经历了 ASP.NET 应用程序 (.NET Framework 4.8.1) 中嵌套Parallel.ForEach的“非常艰难”的调试。

3 天后,我发现证据表明嵌套 

Parallel.ForEach

语句

will not
遵守在每个线程的开始/结束处运行 localInit
localFinally
的规则(对于每个分区)。
不知何故,它将共享同一个线程来处理父级和嵌套

Parallel.ForEach

语句的项目,乱序运行

localInit
localFinally
,这意味着:它将运行
localInit
一次,开始处理父级items,然后在同一线程中再次运行
localInit
,开始处理嵌套项目。然后它将运行
localFinally
来完成父循环或嵌套循环的处理,最后将运行 localFinally
again
来完成。
如果您正在处理共享的 ThreadStatic<T> 变量(这就是我的情况),这将导致 

重大问题

所以,我的建议是:

不要
嵌套

Parallel.ForEach

陈述,除非你确切地知道你在做什么。

    
© www.soinside.com 2019 - 2024. All rights reserved.