我有一个循环,我正在尝试并行化,并在其中我填充容器,比如一个STL地图。然后考虑下面的简单伪代码,其中T1和T2是一些任意类型,而f和g是整数参数的一些函数,分别返回T1,T2类型:
#pragma omp parallel for schedule(static) private(i) shared(c)
for(i = 0; i < N; ++i) {
c.insert(std::make_pair<T1,T2>(f(i),g(i))
}
这看起来相当直接,似乎应该是平凡的并行化,但它并没有像我预期的那样加速。相反,它导致我的代码中的运行时错误,因为容器中填充了意外值,可能是由于竞争条件。我甚至尝试过设置障碍而不是什么,但一切都没有用。唯一允许它工作的是使用 critical 指令,如下所示:
#pragma omp parallel for schedule(static) private(i) shared(c)
for(i = 0; i < N; ++i) {
#pragma omp critical
{
c.insert(std::make_pair<T1,T2>(f(i),g(i))
}
}
但是这种渲染在上面的例子中使用omp的全部意义毫无用处,因为一次只有一个线程正在执行循环的大部分(容器插入语句)。我在这里错过了什么?如果不改变编写代码的方式,有人可以解释一下吗?
答案 0 :(得分:3)
除非f()
和g()
是非常昂贵的函数调用,否则这个特殊的示例并不适合并行化。
STL容器不是线程安全的。这就是你获得比赛条件的原因。因此,需要同步访问它们 - 这使您的插入过程本身是连续的。
正如另一个回答所提到的,并行性有很多开销。因此,除非f()
和g()
非常昂贵,否则您的循环不会做足够的工作来抵消并行性的开销。
现在假设f()
和g()
是非常昂贵的调用,那么你的循环可以像这样并行化:
#pragma omp parallel for schedule(static) private(i) shared(c)
for(i = 0; i < N; ++i) {
std::pair<T1,T2> p = std::make_pair<T1,T2>(f(i),g(i));
#pragma omp critical
{
c.insert(p);
}
}
答案 1 :(得分:1)
运行多线程代码可以让您考虑线程安全性和对变量的共享访问。只要你从多个线程开始插入c
,集合就应该准备好接受这种“同时”调用并保持其数据一致,你确定它是这样做的吗?
另一个原因是并行化有自己的开销,当你尝试在多个线程上运行一个非常小的任务时你不会获得任何东西 - 分裂和同步的成本可能会导致更高的总执行时间为了这项任务。
答案 2 :(得分:1)
c
显然会有数据竞赛。 STL映射不是线程安全的。在多个线程中同时调用insert
方法将产生非常不可预测的行为,主要是崩溃。
是的,为了避免数据争用,您必须拥有(1)类似#pragma omp critical
的互斥锁,或(2)并发数据结构(也称为无外观数据)结构)。但是,并非所有数据结构都可以在当前硬件中无锁定。例如,TBB提供tbb::concurrent_hash_map
。如果您不需要订购密钥,您可以使用它并且可以获得一些加速,因为它没有传统的互斥锁。
如果您只能使用哈希表并且表格非常庞大,您可以采用类似于的方法(请参阅this link了解减少的概念)。哈希表不关心插入的顺序。在这种情况下,您为每个线程分配多个哈希表,并让每个线程并行插入N /#个线程项,这将提供加速。通过并行访问这些表格也可以轻松查找。