嵌套 Parallel.For 循环中的资源共享 C#
Resource sharing in nested Parallel.For loop C#
背景
我有一段代码是高度可并行化的,我发现大多数时候我只使用一个 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为上边界的内循环以及名为[=34=的求和变量]共享资源。这种情况下的执行顺序并不重要,因为加法是可交换的,而且我没有看到任何明显的理由应用 Amdahl 定律,因为两个循环的所有实例组合 (i, j) 都可以独立计算。
问题
在这种情况下使用嵌套的 Parallel.For() 循环是否明智,或者我应该只使用它而不是外部循环(或分别只在内部循环)?
我唯一关心的是 sharedResource 因为我没有从文档中深入了解 Parallel.For() 的工作原理.另一件重要的事情是,如果我确实使用两个 Parallel.For() 循环,一些实例将由于 break 几乎立即完成,而其他实例将花费更多时间。它能平衡这个吗?
您可以使用一些启用了负载平衡的自定义分区程序,并在 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 在确定每次迭代应在哪个线程上提供额外成本 运行。所以有时候最好有一些内部循环 single-threaded 。在 LINQ 示例中,在某些情况下,内部 AsParallel
可能确实会提供额外的成本。
是使用嵌套并行循环,仅并行化内部循环还是仅并行化外部循环,在很大程度上取决于数据的性质。嵌套的并行循环设计得相当好。例如,如果外循环和内循环的并行度均为 8 - 这并不意味着当嵌套时它们将在 8x8=64 线程上处理项目,就像天真地看这个时可能会想的那样。
您应该衡量所有选项在您的特定数据集上的性能,并找出最适合您的选项。
请注意,Parallel.For
循环分区间隔一定数量的范围(取决于并行度),然后这些范围在单独的线程上并行执行。这意味着:如果您的项目的处理时间是分布式的 non-evenly - 某些范围可能比其他范围更快完成。假设您 运行 的并行度为 4,并处理 100 个项目,其中前 75 个 return false
用于 someCondition
,因此执行时间为 0,而最后 25 return true
。结果,前 3 个范围将立即完成,所有实际工作的最后一个范围将在一个线程上执行,基本上使整个事情顺序进行。
如果预期分布不均匀,您可以使用 Parallel.ForEach
和 "real" IEnumerable
代替(我的意思是它不是数组或列表,而是真实的 "lazy" IEnumerable
):
Parallel.ForEach(Enumerable.Range(0, i), j => {...})
但请注意,在均匀分布的数据上,它会比 pre-partitioned 版本慢。
如果 run-time 分布不均,嵌套 Parallel.For
也可能有帮助,但同样 - 您必须根据真实数据衡量每个选项并选择最佳的。
至于线程安全。当然,这个
sharedResource += someFunction(i, j);
在并行循环中不是线程安全的。如果 someFunction
很快,那么在这里使用 lock
可能会大大降低性能,而且无论如何都没有必要。要么只使用
Interlocked.Add(ref sharedResource, someFunction(i, j))
或者您可以使用 Parallel.For
`Parallel.ForEach` 的重载,允许每个 运行ning 线程累积值,然后聚合结果。例如:
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));
});
背景
我有一段代码是高度可并行化的,我发现大多数时候我只使用一个 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为上边界的内循环以及名为[=34=的求和变量]共享资源。这种情况下的执行顺序并不重要,因为加法是可交换的,而且我没有看到任何明显的理由应用 Amdahl 定律,因为两个循环的所有实例组合 (i, j) 都可以独立计算。
问题
在这种情况下使用嵌套的 Parallel.For() 循环是否明智,或者我应该只使用它而不是外部循环(或分别只在内部循环)?
我唯一关心的是 sharedResource 因为我没有从文档中深入了解 Parallel.For() 的工作原理.另一件重要的事情是,如果我确实使用两个 Parallel.For() 循环,一些实例将由于 break 几乎立即完成,而其他实例将花费更多时间。它能平衡这个吗?
您可以使用一些启用了负载平衡的自定义分区程序,并在 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 在确定每次迭代应在哪个线程上提供额外成本 运行。所以有时候最好有一些内部循环 single-threaded 。在 LINQ 示例中,在某些情况下,内部 AsParallel
可能确实会提供额外的成本。
是使用嵌套并行循环,仅并行化内部循环还是仅并行化外部循环,在很大程度上取决于数据的性质。嵌套的并行循环设计得相当好。例如,如果外循环和内循环的并行度均为 8 - 这并不意味着当嵌套时它们将在 8x8=64 线程上处理项目,就像天真地看这个时可能会想的那样。
您应该衡量所有选项在您的特定数据集上的性能,并找出最适合您的选项。
请注意,Parallel.For
循环分区间隔一定数量的范围(取决于并行度),然后这些范围在单独的线程上并行执行。这意味着:如果您的项目的处理时间是分布式的 non-evenly - 某些范围可能比其他范围更快完成。假设您 运行 的并行度为 4,并处理 100 个项目,其中前 75 个 return false
用于 someCondition
,因此执行时间为 0,而最后 25 return true
。结果,前 3 个范围将立即完成,所有实际工作的最后一个范围将在一个线程上执行,基本上使整个事情顺序进行。
如果预期分布不均匀,您可以使用 Parallel.ForEach
和 "real" IEnumerable
代替(我的意思是它不是数组或列表,而是真实的 "lazy" IEnumerable
):
Parallel.ForEach(Enumerable.Range(0, i), j => {...})
但请注意,在均匀分布的数据上,它会比 pre-partitioned 版本慢。
如果 run-time 分布不均,嵌套 Parallel.For
也可能有帮助,但同样 - 您必须根据真实数据衡量每个选项并选择最佳的。
至于线程安全。当然,这个
sharedResource += someFunction(i, j);
在并行循环中不是线程安全的。如果 someFunction
很快,那么在这里使用 lock
可能会大大降低性能,而且无论如何都没有必要。要么只使用
Interlocked.Add(ref sharedResource, someFunction(i, j))
或者您可以使用 Parallel.For
`Parallel.ForEach` 的重载,允许每个 运行ning 线程累积值,然后聚合结果。例如:
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));
});