背景
我有一段高度可并行化的代码,我发现大部分时间我只使用100%的一个核心,而其余的则什么都不做。为了解决这个问题,我已经修改了多线程,实现了信号量,并且没有意识到Parallel.For()比我的任何解决方案都更精细,更高效。
守则
为了简化,我只会编写在结构上重要的代码片段。
int sharedResource = 0;
for (int i = 0; i < someMax; i++)
{
for (int j = 0; j <= i; j++)
{
if (someCondition(i, j))
sharedResource += someFunction(i, j);
else break;
}
}
所有模糊命名的函数或多或少只是数学方程式,具有时间复杂度O(1)。
重要细节
注意具有变量 i 的内部循环作为上边界以及名为 sharedResource <的总和变量< / strong>即可。在这种情况下执行顺序并不重要,因为加法是可交换的,我没有看到任何明显的理由要求 Amdahl定律,因为两个循环的所有实例组合(i,j)都可以独立计算。
问题
在这种情况下使用嵌套的Parallel.For()循环是否明智,还是应该只使用它而不是外部循环(或仅在内部循环)?
我唯一关心的是 sharedResource ,因为我没有深入了解Parallel.For()如何从文档中运行。另一个重要的事情是,如果我使用两个Parallel.For()循环,由于中断,一些实例几乎会立即完成,而其他实例将花费更多时间。它可以平衡这个吗?
答案 0 :(得分:3)
是否使用嵌套并行循环,仅对内部或仅外部循环进行并行化,在很大程度上取决于数据的性质。嵌套并行循环旨在合理地工作。例如,如果外部和内部循环都具有8的并行度,例如 - 它并不意味着当嵌套时它们将处理8x8 = 64个线程上的项目,正如人们在天真地看待它时所想的那样。
您应该衡量特定数据集中所有选项的性能,并找出最适合您的选项。
注意Parallel.For
循环分区在一定数量的范围内(取决于并行度),然后这些范围在不同的线程上并行执行。这意味着:如果物品的处理时间分布不均匀 - 某些范围可能比其他范围完成得快得多。假设您运行并行度4,并处理100个项目,其中前75个项false
返回someCondition
,因此需要0次执行,而最后25个返回true
。结果,前3个范围将立即完成,所有实际工作的最后范围将在一个线程上执行,基本上使整个事物顺序完成。
如果预计分布不均,您可以使用Parallel.ForEach
代替“真实”IEnumerable
(实际上我的意思是它不是数组或列表而是实际的“懒惰”IEnumerable
):< / p>
Parallel.ForEach(Enumerable.Range(0, i), j => {...})
但请注意,在均匀分布的数据上,它将比预分区版本慢。
如果运行时分布不均匀,嵌套Parallel.For
也可能会有所帮助,但您必须再次测量实际数据中的每个选项并选择最佳选项。
关于线程安全。当然,这个
sharedResource += someFunction(i, j);
在并行循环中不是线程安全的。如果lock
速度很快,那么在这里使用someFunction
可能会大幅降低性能,而且无论如何都不需要。要么只是使用
Interlocked.Add(ref sharedResource, someFunction(i, j))
或者你可以使用Parallel.For
`Parallel.ForEach`的重载,它允许每个正在运行的线程累积值,然后聚合结果。例如:
Parallel.For(0, 100, (i, outerState) =>
{
Parallel.ForEach(Enumerable.Range(0, i), () => 0, (j, innerState, subTotal) =>
{
if (someCondition(i, j))
return subTotal + someFunction(i, j);
else {
innerState.Break();
return subTotal;
}
}, subTotalOfThread => Interlocked.Add(ref sharedResource, subTotalOfThread));
});
答案 1 :(得分:1)
您可以使用一些启用了负载平衡的自定义分区程序,并在Parallel.ForEach
循环中使用它。负载平衡确保每个核心都忙,直到执行结束。例如:
int sharedResource = 0;
var iterations = Enumerable.Range(0, someMax);
//this creates partitioner with load balancing (true is default for IEnumerable really)
var customPartitioner = Partitioner.Create(iterations, true);
Parallel.ForEach(customPartitioner, i =>
{
for (int j = 0; j <= i; j++)
{
if (someCondition(i, j))
Interlocked.Add(ref sharedResource, someFunction(i, j));
else break;
}
});
在您的示例中,赋值运算符实际上不是线程安全的,因此我改为使用Interlocked.Add
。
您还可以编写一些可以通过LINQ设计并行化的功能代码。请注意,没有任何共享资源或线程同步,因为FP中没有状态。
var result = customPartitioner
.AsParallel()
.Select(i => Enumerable.Range(0, i + 1)
.AsParallel()
.TakeWhile(j => someCondition(i, j))
.Sum(j => someFunction(i, j)))
.Sum();
您还需要考虑的一件事是线程创建成本。您创建的线程越多,处理器的时间浪费就越多,而不是实际工作。此外, Parallel.Foreach 在确定每次迭代应运行的线程时提供了额外的成本。所以有时最好有一些内循环单线程。在LINQ示例中,在某些情况下,内部AsParallel
可能会提供额外的成本。