我有一些代码,目前正在针对多核架构中的并发性进行优化。在我的一堂课中,我发现了一个嵌套的
foreach
循环。基本上,外循环遍历一组 NetworkInterface
对象。内部循环迭代网络接口 IP 地址。
这让我思考,嵌套
Parallel.ForEach
循环一定是个好主意吗?阅读本文后(同一列表中的嵌套 Parallel.ForEach 循环?)我仍然不确定在效率和并行设计方面什么适用于何处。此示例将 Parallel.Foreach
语句应用于列表,其中两个循环都对该列表执行操作。
在我的示例中,循环正在做不同的事情,所以我应该:
Parallel.ForEach
循环?Parallel.ForEach
并保持内部循环不变?Parallel.ForEach 不一定并行执行——它只是一个请求,如果可能的话,这样做。因此,如果执行环境没有CPU能力来并行执行循环,它就不会这样做。
如果循环上的操作不相关(即,如果它们是独立的并且不相互影响),则我认为在内循环和外循环上使用 Parallel.ForEach 没有问题。
这确实取决于执行环境。如果您的测试环境与生产环境足够相似,您可以进行时序测试,然后确定要做什么。如有疑问,请进行测试;-)
祝你好运!
答案是,这取决于;
线程并不便宜,它们需要时间来创建,并且需要内存来存在。 如果您没有对这些 IP 地址执行计算成本较高的操作,并且使用错误的集合类型进行并发访问,那么您几乎肯定会减慢应用程序的速度。
StopWatch
来帮助您回答这些问题。
我的建议是遵循第二种方法:仅并行化外循环,并保持内循环顺序(
for
/foreach
)。不要将 Parallel.ForEach
将一个循环放在另一个内部。原因是:
并行化增加了开销。每个
Parallel
循环必须同步 source
的枚举、启动 Task
、监视取消/终止标志等。通过嵌套 Parallel
循环,您将多次支付此成本。
MaxDegreeOfParallelism
选项不是影响子循环的环境属性。它仅限制单个循环。因此,如果您有一个带有 Parallel
的外部 MaxDegreeOfParallelism = 4
循环和一个也带有 Parallel
的内部 MaxDegreeOfParallelism = 4
循环,则内部 body
可能会同时调用 16 次 (4 * 4
)。仍然可以通过使用相同的 TaskScheduler
配置所有循环来强制实施合理的上限,特别是使用共享 ConcurrentScheduler
实例的 ConcurrentExclusiveSchedulerPair
属性。
如果出现异常,您将得到一个深层嵌套的
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# 中的并行树遍历。
我刚刚经历了 ASP.NET 应用程序 (.NET Framework 4.8.1) 中嵌套Parallel.ForEach
的“非常艰难”的调试。
Parallel.ForEach
语句
will not遵守在每个线程的开始/结束处运行
localInit
和 localFinally
的规则(对于每个分区)。不知何故,它将共享同一个线程来处理父级和嵌套Parallel.ForEach
语句的项目,乱序运行
localInit
和localFinally
,这意味着:它将运行localInit
一次,开始处理父级items,然后在同一线程中再次运行 localInit
,开始处理嵌套项目。然后它将运行 localFinally
来完成父循环或嵌套循环的处理,最后将运行 localFinally
again来完成。如果您正在处理共享的
ThreadStatic<T>
变量(这就是我的情况),这将导致 重大问题。 所以,我的建议是:
不要嵌套
Parallel.ForEach
陈述,除非你确切地知道你在做什么。