使用OpenMP不能同时读取和写入位置

时间:2017-02-21 23:36:20

标签: c multithreading openmp

我有一个在OpenMP并行for循环中写入的变量。循环是这样的,没有其他线程会写入它,但它们都在不同的时间读取它。根据一些SO答案,同时读取和写入某个位置可能会导致读取值被破坏。这是真的?如何在保持尽可能多的性能的同时确保不会发生这种情况?

我查看了规范中的OpenMP原子构造。关于atomic write,它说:

  

带有write子句的原子构造强制对x指定的位置进行原子写入,而不管本地机器字大小如何。

因为我知道只有一个线程写入该位置,所以这不是我需要的。

对于atomic update,它说

  

只有读取和写入x指定的位置   相互原子地进行。

这是否意味着读取不会与写入重叠?如果这是真的,我认为这就是我需要的。在这种情况下,我可以在atomic update(而不是x = expr;)这样的语句中使用x += expr;吗?

任何人都可以帮助澄清atomic update究竟做了什么,和/或我应该在我的案例中使用什么?

我读过Is OpenMP atomic write needed if other threads read only the shared data?,但我不清楚。

编辑: @Zulan的回答明确了atomic update的作用,但就我而言,我需要保护读取语句不被写入代码中其他所做的相同位置。我不需要保护写入免于其他写入(原子写入),因为没有其他线程写入该位置,并且我不需要保护读取不被读取。我怎么能这样做?

EDIT2:我知道我可能不太清楚。所以代码片段。我所拥有的是“异步”迭代。当我在下面的数组x中读取一个位置时,我只需要一个未损坏的值;它可能已经更新或没有任何次数。

double *x;

#pragma omp parallel for default(shared)
for(int i = 0; i < sizeof_x; i++)
{
  int j = some_func1(i);
  int k = some_func2(i);
  double temp = some_const*x[j] + x[k];
  x[i] = temp;
}

在这里,我不需要x[i]的原子写入,因为它只由一个线程写入,我不想要x[j]x[k]的原子读取,因为同时读取没问题。但我想确保当我读取x[j]x[k]时,它不会被另一个线程在那个时钟周期内写入。这是我需要关心的事情(我认为是这样),如果是的话,该怎么办呢?

2 个答案:

答案 0 :(得分:1)

我会尝试解决您的具体问题,但the related answer的一般建议仍然存在。

  

根据一些SO答案,同时读取和写入某个位置可能会导致读取值被破坏。

是。无论是否在实践中,都高度依赖于体系结构,数据类型,编译器和优化。

  

如何在保持尽可能多的性能的同时确保不会发生这种情况?

确保使用原子读取,写入和更新(和捕获)对变量的所有访问的可移植和清洁方式。

  

这是否意味着读取不会与写入重叠?任何人都可以帮助澄清原子更新究竟做了什么,和/或我应该在我的案例中使用什么?

没有。 atomic update不适用于纯分配。它仅适用于读取值,对其执行操作并再次写入的语句。更新的原子性指的是该特定语句的读/写,而不是代码中其他地方的任何其他读取。您应该使用atomic write进行分配,将atomic read用于该位置的所有其他读取。

我希望澄清您的具体问题。再次注意,原子性并不能保证内存更新的可见性。如果依赖于最终在其他线程上看到更新的值,则还必须考虑显式或隐式内存刷新。

恕我直言,OpenMP标准写得非常好。如果你想要最准确的措辞,你应该参考它(1.4和2.13.6)。

修改:关于您的具体示例,您需要保护每次读写x。对一个位置的并发读写是数据争用,可能导致不确定的值。从标准:

  

2.13.6 atomic Construct

     

[...]为避免竞争条件,x指定的可能并行发生的位置的所有访问都必须使用原子结构进行保护。

想象一下,对x的读/写是通过两个独立的读/写来实现的。标准明确允许这一点(1.4.1,&#34;对变量的单一访问可以使用多个加载或存储指令实现&#34;):

x_j.low  = laod x[j].low;
x_j.high = load x[j].high;
x_k.low  = load x[k].low;
x_k.high = load x[k].high;
temp = some_const * x_j + x_k;
store x[i].low = temp.low;
store x[i].high = temp.high;

现在想象一下并发情况:

Thread 0 [i = 10]             | Thread 1 [j = 10]
----------------------------------------------------------
store x[10].low = temp.low;   |
                              | x_j.low = load x[10].low
                              | x_j.high = load x[10].high
store x[10].high = temp.high; |

只能锁定任何一个线程,你无法阻止读取损坏的值,你必须在两边都这样做。

请注意,这只是一个例子。对于实践中的某些类型,原子代码和非原子代码之间可能完全没有区别。但是标准要求你这样做。

答案 1 :(得分:0)

这是2001年由Chandra,Dagum,Kohr,Mayden,McDonald,Menon撰写的OpenMP并行编程的教科书定义。

  

原子指令旨在高效访问机器原语/指令(intel X86上的比较交换CMPXCHG),允许处理器执行读取,修改,写入操作,例如内存位置的增量原子时尚。它使用硬件支持在更新期间获得对单个位置的独占访问。

     

原子指令与关键指令一样,是表达互斥的另一种方式,并不提供任何其他功能。虽然critical指令包含任意代码块,但只有当临界区包含更新标量变量的单个赋值语句时,才能应用原子。

     

对使用原子指令的限制确保可以将赋值转换为一系列机器指令,以原子方式读取,修改和写入指定的内存位置。   关于同步,用户必须确保使用相同的同步指令(原子或关键)一致地保护对此内存位置的所有冲突访问。

     

包含使用原子指令的单个赋值语句的关键部分通常更有效,而且从不会更糟。但是具有多个赋值的临界区不能转换为使用单个原子指令,它需要每个语句的原子指令 - 假设每个语句都满足使用原子指令的要求。   一般指导是在更新单个位置或仅更新几个位置时使用原子,并在更新许多位置时使用关键指令。

据说,到目前为止,您已经确定了您的赋值语句,其中您写入了一些内存位置,并且您正在使用原子指令。然后,您需要在其他任何位置使用原子指令,以便在将对该内存位置执行读取的所有其他线程中,该读取赋值具有原子指令。而在其他地方,无处不在的范围是代码的关键部分(非关键指令),其中并行性发生直到所有线程同步为止。