C/C++基本类型是原子的吗?

car*_*ose 58 c c++ multithreading atomic

是C/C++的基本类型,如int,double等,原子,如线程?

他们是否免于数据竞赛; 也就是说,如果一个线程写入这种类型的对象而另一个线程从中读取,那么行为是否定义明确?

如果没有,它是否依赖于编译器或其他东西?

Jam*_*son 69

不,基本数据类型(例如int,double)不是原子的,请参阅std::atomic.

相反,你可以使用std::atomic<int>std::atomic<double>.

注意: std::atomic是在C++ 11中引入的,我的理解是在C++ 11之前,C++标准根本不承认存在多线程.


正如@Josh所指出的那样,std::atomic_flag是一种原子布尔类型.与专业化不同,它保证无锁std::atomic.


引述的文件是:http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf.我很确定标准不是免费的,因此这不是最终/官方版本.

1.10多线程执行和数据竞争

  1. 如果其中一个修改内存位置(1.7)而另一个读取或修改相同的内存位置,则两个表达式评估会发生冲突.
  2. 该库定义了许多原子操作(第29条)和对互斥锁(第30条)的操作,这些操作被特别标识​​为同步操作.这些操作在使一个线程中的分配对另一个线程可见时起到特殊作用.在一个或多个存储器位置上的同步操作是消费操作,获取操作,释放操作,或获取和释放操作两者.不具有相关联的存储器位置中的同步操作是围栏的,可以是一个获取栅栏,释放栅栏,或两者的获取和释放围栏.此外,还有轻松的原子操作,它们不是同步操作,还有原子读 - 修改 - 写操作,它们具有特殊的特性.


  1. 如果两个动作是潜在的并发
    (23.1) -它们是由不同的线程执行,或者
    (23.2) -它们是未测序,和至少一个由信号处理器执行的.
    程序的执行包含数据竞争,如果它包含两个可能同时发生冲突的动作,其中至少有一个不是原子的,并且除了下面描述的信号处理程序的特殊情况之外,它们都不会发生在另一个之前.任何此类数据争用都会导致未定义的行为.

29.5原子类型

  1. 应当有原子模板的显式特化的整数类型``字符,signed char,unsigned char,short,unsigned short,int,unsigned int,long,unsigned long,long long,unsigned long long,char16_T, ,char32_t,wchar_t和任何其他类型的标头中的类型定义需要<cstdint>.对于每个整数类型积分,专门化atomic<integral>提供适合于整数类型的附加原子操作.应有专门化atomic<bool>,提供29.6.1中规定的一般原子操作.


  1. 应该有原子类模板的指针部分特化.这些特化应具有标准布局,普通默认构造函数和普通析构函数.它们应各自支持聚合初始化语法.

29.7标志类型和操作

  1. 对atomic_flag类型的对象的操作应该是无锁的.[注意:因此操作也应该是无地址的.没有其他类型需要无锁操作,因此atomic_flag类型是符合此国际标准所需的最低硬件实现类型.其余类型可以使用atomic_flag进行模拟,但具有不太理想的属性. - 结束说明]

  • @AndrewHenle:我知道.但是句子"是唯一的C++对象"是错误的.他们不是.反例:§30.4.1.2.5[thread.mutex.requirements.mutex]:"实现应提供锁定和解锁操作,如下所述.为了确定数据竞争的存在,这些操作表现为原子操作(1.10) )".(再次,我正在扮演魔鬼的拥护者并要求更正式的答案,而不是随机维基上的摘要中的c&p.) (6认同)
  • *原子类型的对象是唯一没有数据竞争*的C++对象.真?那么`std :: mutex`怎么样?(在这里扮演魔鬼的拥护者,只是这句话需要一点爱和一些参考标准.) (4认同)

And*_*nle 16

由于C(尽管不在标签中)也在(当前)提及C,因此C标准规定:

5.1.2.3程序执行

...

当通过接收信号来中断抽象机器的处理时,既不是无锁原子对象也不是类型的对象的值volatile sig_atomic_t是未指定的,浮点环境的状态也是如此.处理程序修改的既不是无锁原子对象也不是类型的任何对象的值volatile sig_atomic_t在处理程序退出时变得不确定,如果处理程序修改了浮点环境并且没有恢复到它的状态,则浮点环境的状态也是如此.原始状态.

5.1.2.4多线程执行和数据争用

...

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

[几页标准 - 一些明确针对原子类型的段落]

程序的执行包含 数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且都不会在另一个之前发生. 任何此类数据争用都会导致未定义的行为.

请注意,如果信号中断处理,则值为"不确定",并且对非显式原子类型的同时访问是未定义的行为.

  • 注意,C11添加了`_Atomic`类型限定符和`<stdatomic.h>`标题... (3认同)

Emi*_* L. 10

什么是原子?

原子,描述具有原子属性的东西.单词atom来自拉丁语atomus,意思是"不可分割".

通常我认为原子操作(不论语言)有两个特质:

原子操作总是不可分割的.

即它以不可分割的方式执行,我相信这就是OP所称的"线程安全".从某种意义上说,当另一个线程查看时,操作会立即发生.

例如,以下操作可能被划分(依赖于编译器/硬件):

i += 1;
Run Code Online (Sandbox Code Playgroud)

因为它可以被另一个线程(在假设的硬件和编译器上)观察为:

load r1, i;
addi r1, #1;
store i, r1;
Run Code Online (Sandbox Code Playgroud)

执行上述操作i += 1而没有适当同步的两个线程可能会产生错误的结果.i=0最初说,线程T1加载T1.r1 = 0,线程T2加载t2.r1 = 0.两个线程将它们各自的r1s 递增1,然后将结果存储到i.虽然已经执行了两个增量,但值i仍然只是1,因为增量操作是可分的.请注意,如果i+=1在其他线程等待操作完成之前和之后已经进行了同步,那么就会观察到一个不可分割的操作.

请注意,即使是简单的写入也可以是不可分割的:

i = 3;

store i, #3;
Run Code Online (Sandbox Code Playgroud)

取决于编译器和硬件.例如,如果地址i未适当对齐,则必须使用未对齐的加载/存储,其由CPU作为若干较小的加载/存储来执行.

原子操作保证了内存排序语义.

非原子操作可以重新排序,并且可能不一定按照在程序源代码中编写的顺序发生.

例如,在"as-if"规则下,只要所有对易失性存储器的访问都按照程序指定的顺序"就好"评估了程序,就允许编译器按其认为合适的方式对存储和加载进行重新排序.根据标准中的措辞.因此,可以重新安排非原子操作,从而破坏关于多线程程序中的执行顺序的任何假设.这就是为什么int在多线程编程中看似无害地使用raw 作为信号变量的原因被打破,即使写入和读取可能是不可分割的,排序可能会根据编译器中断程序.原子操作根据指定的内存语义强制执行对其周围操作的排序.见std::memory_order.

CPU还可以在该CPU的内存排序约束下重新排序内存访问.您可以在Intel 64和IA32架构软件开发人员手册第8.2节中找到x86架构的内存排序约束,从第2212页开始.

原始类型(int,char等等)不是原子

因为即使它们在某些条件下可能具有不可分割的存储和加载指令,甚至可能有一些算术指令,它们也不能保证存储和加载的顺序.因此,在没有适当同步的情况下在多线程上下文中使用它们是不安全的,以保证其他线程观察到的内存状态是您认为在那个时间点的内存状态.

我希望这能解释为什么原始类型不是原子的.

  • @DavidSchwartz当然,_caches_是连贯的; 它不是商店缓冲区.即使在x86上 - 请参阅"系统编程指南"第8.2章中的示例8-3和8-5.虽然它几乎不是像Alpha或POWER这样的内存排序的狂野西部,但是说所有核心总是在_all_次读取相同的值仍然严格错误. (4认同)
  • @DavidSchwartz确切地说,确切的措辞是错误的,但关键是在一个核心写入之后的一段时间,其中读取_由不同的核心_仍然可以得到旧的值("之后"在第一个读取的意义上核心将返回新值).所以商店既发生了又没发生,取决于你观察的地方.我只想指出[第2217页](http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer -manual-325462.pdf)并立即闭嘴;) (4认同)
  • @Notlikethat要么你试图准确地解释实际硬件是如何工作的,要么你不是。如果是,那么你就失败了,因为这与缓存无关。如果您不是,那么这都是不必要的复杂化,您最好谈论标准。这似乎是不必要的挑剔,但当这种错误信息成为其他误解实际硬件工作原理的人引用的错误信息来源时,我不得不纠正这种错误信息数百次。 (2认同)

M.M*_*M.M 6

到目前为止,我在其他答案中还没有提到的其他信息:

std::atomic<bool>例如,如果您使用并且bool在目标架构上实际上是原子的,那么编译器将不会生成任何冗余栅栏或锁。将生成与普通bool.

换句话说,std::atomic如果确实需要平台上的正确性,使用只会降低代码的效率。所以没有理由避免它。