C ++是否保证从两个线程访问数组的相邻元素是安全的

问题描述 投票:3回答:4

就C ++标准而言(C ++ 11及更高版本,我猜,因为在不考虑线程之前),并发写入数组的不同的,可能相邻的元素是否安全?

例如:

#include <iostream>
#include <thread>

int array[10];

void func(int i) {
   array[i] = 42;
}

int main() 
{
   for(int i = 0; i < 10; ++i) {
      // spawn func(i) on a separate thread
      // (e.g. with std::async, let me skip the details)
   }
   // join

   for(int i = 0; i < 10; ++i) {
      std::cout << array[i] << std::endl; // prints 42?
   }

   return 0;
}

在这种情况下,语言是否保证数组的不同元素的写入不会导致竞争条件?它是否适用于任何类型,或者是否有任何安全要求?

c++ multithreading c++11 thread-safety language-lawyer
4个回答
1
投票

是。

来自https://en.cppreference.com/w/cpp/language/memory_model

当表达式的评估写入内存位置而另一个评估读取或修改相同的内存位置时,表达式会发生冲突。具有两个相互冲突的评估的程序具有数据竞争,除非[...]

然后:

内存位置是

  • 标量类型的对象(算术类型,指针类型,枚举类型或std :: nullptr_t)
  • 或非零长度的最大连续的位域序列

因此,如果数组的元素存储在不同的内存位置,则不会产生冲突的评估。

array是:

形式为T a[N];的声明,声明一个数组对象,它由N个连续分配的类型为T的对象组成。

由于两个不同的对象不能具有相同的地址,因此它们及其组成者不能具有相同的存储位置。这保证了对早期要求的满意度。

此外,对象可以包含多个内存位置,因此您甚至可以让两个线程对同一对象的不同成员进行操作!

请注意,为了使您的示例正确,连接也必须正确编写,但它与数组的相邻元素无关,而是在同一个元素上运行,所以我猜这超出了问题的范围。


个人说明:顺便说一句。如果不能保证这一点,那么如果不在标准库中进行无用的并行计算,则会严重限制。


1
投票

数据竞争只发生在同一个内存位置,即只有x才能在两个glvalues y&x == &y上进行数据竞争。

[intro.races]/2

如果其中一个修改内存位置而另一个读取或修改相同的内存位置,则两个表达式评估会发生冲突。

[intro.races]/21

如果一个程序的执行包含两个可能同时发生冲突的动作,那么该程序的执行包含数据竞争[...]

其余部分不适用于此处。所以你的阵列没有数据竞争。


0
投票

是的,但“确定”并不意味着这样做很聪明。

有几个问题需要考虑,也许最重要的一个是CPU缓存。在例如x86,缓存行长度为64字节,因此每个线程应该例如处理与缓存行长度匹配的数组块,以避免例如虚假分享。

这是一个例子:false sharing SO question/answer


0
投票

并行访问不同线程上的连续元素是安全的,但如果频繁发生,则可能导致代码中出现性能问题。这是由于现代CPU上并行性的基本限制。

对于大多数程序来说,内存访问是一个主要的瓶颈。必须仔细编写代码的性能关键部分,以避免过多的缓存未命中。缓存有多个级别,每个级别比前一个级别更快。但是,当数据不在缓存中时,或者数据可能已被另一个CPU更改时,必须将其重新加载到缓存中。

CPU无法跟踪每个字节的状态,因此它会跟踪称为高速缓存行的字节块。如果高速缓存行中的任何字节由单独的CPU更改,则必须重新加载以确保同步。

在不同线程上访问单独的字节只会导致重新加载,如果它们位于同一缓存行中。并且因为高速缓存行是连续的,所以从不同的线程访问连续的元素通常会导致必须将存储器重新加载到高速缓存中。 This is called false sharing, and should be avoided in parallel code if performance is a concern.

话虽这么说,如果很少发生它可能很好,你应该在优化它之前对你的代码进行基准测试和测试。

© www.soinside.com 2019 - 2024. All rights reserved.