嵌套的Parallel.ForEach循环

27

我有一些代码,目前正在优化以适应多核架构的并发性。在我的一个类中,我发现了一个嵌套的foreach循环。基本上,外部循环遍历NetworkInterface对象数组。内部循环遍历网络接口IP地址。

这让我想到,嵌套的Parallel.ForEach循环一定是个好主意吗?在阅读了这篇文章(Nested Parallel.ForEach Loops on the same list?)之后,我仍然不确定在效率和并行设计方面哪种方法更适用。这个例子是关于将Parallel.Foreach语句应用于列表,其中两个循环都在该列表上执行操作。

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

  1. 使用嵌套的Parallel.ForEach循环?
  2. 在父循环上使用Parallel.ForEach,并保留内部循环不变?

2
你能用计时器测试解决方案吗?这样你就会知道它是否值得。 - mike00
3个回答

33

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

如果循环中的操作彼此无关(即它们是单独的且不相互影响),我认为在内部和外部循环都使用Parallel.ForEach是没有问题的。

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

祝你好运!


3
完全不同意。是的,Parallel.Foreach背后的调度程序可能不会生成单独的线程,但你要花费更多的开销来使用线程或调度程序,而没有任何科学数据来支持它。 - M Afifi
3
请再次阅读我的答复,谢谢。 - Roy Dictus
1
并行化内部循环将会增加一些开销,因此过度并行化内部循环可能会导致更差的性能。 - svick
2
在这种情况下,我会优化您认为最常见的设置。这意味着采取一个“平均”的服务器PC进行测试和测量。在这种情况下,可能嵌套并行循环确实会导致开销。如果您事先知道您的代码将在大型硬件上运行,则并行性肯定会得到回报。 - Roy Dictus
1
@activwerx:理论上来说这是可能的——你可以请求硬件信息并相应地采取行动——但在实践中,你不知道是否有权访问那些核心,即使你知道了,你也不能确定parallel.foreach是否会强制进行优化。只有在专用硬件上才能确保这一点,或者当你事先知道管理员将为你的进程分配n个核心时... - Roy Dictus
显示剩余2条评论

3
答案是,这取决于以下几点:
  1. 你获取IP地址后要用它做什么?
  2. 每个步骤需要多长时间?
线程并不便宜,创建线程需要时间和内存。如果你对这些IP地址没有进行计算密集型的操作,并且在并发访问时使用了错误类型的集合,那么你几乎肯定会拖慢应用程序的速度。
使用 StopWatch 来帮助你回答这些问题。

5
线程的创建成本很高,这正是为什么Parallel.ForEach()使用线程池的原因,因此创建新线程很可能不会成为问题。 - svick

0

我的建议是采用第二种方法:仅并行化外部循环,并保持内部循环(for/foreach)的顺序执行。不要将Parallel.ForEach循环嵌套在另一个循环中。原因如下:

  1. 并行化会增加开销。每个 Parallel 循环都必须同步枚举 source、启动 Task、监视取消/终止标志等。通过嵌套 Parallel 循环,您将多次支付此成本。

  2. 限制并行程度变得更加困难。MaxDegreeOfParallelism 选项不是影响子循环的环境属性。它仅限制单个循环。因此,如果您有一个外部 Parallel 循环,其中 MaxDegreeOfParallelism = 4,内部也有一个 Parallel 循环,其 MaxDegreeOfParallelism = 4,那么内部的 body 可能会同时调用 16 次(4 * 4)。仍然可以通过使用相同的 TaskScheduler 配置所有循环,并具体使用共享 ConcurrentExclusiveSchedulerPair 实例的 ConcurrentScheduler 属性来强制执行合理的上限。

  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之间)。因此,该方法具有某些场景下令人信服的特征,但对于其他场景则不太适合。

还有另一种值得一提的情况是,当外部和内部序列中的对象类型相同时,并且存在父子关系。在这种情况下,请查看以下问题:Parallel tree traversal in C#


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接