C / C ++基本类型,例如int
,double
等,是否为原子,例如线程安全?
他们是否免于数据竞赛;也就是说,如果一个线程写入这种类型的对象而另一个线程从中读取,那么行为是否定义明确?
如果没有,它是否依赖于编译器或其他东西?
答案 0 :(得分:68)
不,基本数据类型(例如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.7)而另一个读取或修改相同的内存位置,则两个表达式评估会发生冲突。
- 该库定义了许多原子操作(第29条)和对互斥锁(第30条)的操作,这些操作被特别标识为同步操作。这些操作在使一个线程中的分配对另一个线程可见时起特殊作用。在一个或多个存储器位置上的同步操作是消费操作,获取操作,释放操作,或获取和释放操作两者。没有相关内存位置的同步操作是围栅,可以是获取围栏,释放围栏,也可以是获取和释放围栏。此外,还有松散的原子操作,它们不是同步操作,还有原子读 - 修改 - 写操作,它们具有特殊的特性。
醇>
- 如果有两个动作可能是并发的
醇>
(23.1) - 它们由不同的线程执行,或者
(23.2) - 它们没有被排序,并且至少有一个是由信号处理程序执行的。
程序的执行包含数据竞争,如果它包含两个可能同时发生冲突的动作,其中至少有一个不是原子的,并且除了下面描述的信号处理程序的特殊情况之外,它们都不会发生在另一个之前。任何此类数据争用都会导致未定义的行为。
- 对于整数类型`char,
中规定的一般原子操作 醇>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>
中typedef所需的任何其他类型。对于每个整数类型积分,特化atomic<integral>
提供适用于整数类型的其他原子操作。应该有一个专门化atomic<bool>
,它提供29.6.1 ..
- 应该有原子类模板的指针部分特化。这些特化应具有标准布局,普通默认构造函数和普通析构函数。它们应各自支持聚合初始化语法。
醇>
- 对atomic_flag类型的对象的操作应该是无锁的。 [注意:因此操作也应该是无地址的。没有其他类型需要无锁操作,因此atomic_flag类型是符合此国际标准所需的最低硬件实现类型。其余类型可以使用atomic_flag进行模拟,但具有不太理想的属性。 - 尾注]
醇>
答案 1 :(得分:16)
由于问题中也提到了C(当前),尽管不在标签中,C Standard表示:
5.1.2.3程序执行
...
当抽象机器的处理因收据而中断时 信号的对象,既不是无锁原子的对象的值 对象和类型
volatile sig_atomic_t
未指定,因为 浮点环境的状态。任何对象的价值 由处理程序修改,既不是无锁原子对象也不是 类型volatile sig_atomic_t
在处理程序时变得不确定 退出,如果它是浮点环境的状态 由处理程序修改而不是恢复到其原始状态。
和
5.1.2.4多线程执行和数据竞赛
...
两个表达式评估 冲突如果其中一个修改了内存位置而另一个读取或修改了相同的内存位置。
[几页标准 - 一些明确针对原子类型的段落]
程序的执行包含一个 data race 如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且都不会发生 在另一个之前。 任何此类数据争用都会导致未定义的行为。
请注意,如果信号中断处理,则值为“不确定”,并且对非显式原子类型的同时访问是未定义的行为。
答案 2 :(得分:10)
Atomic,描述具有原子属性的东西。单词atom来自拉丁语atomus,意思是“不可分割的”。
通常我认为原子操作(不论语言)有两个特质:
即。它是以不可分割的方式执行的,我相信这就是OP所说的“线程安全”。从某种意义上说,当另一个线程查看时,操作会立即发生。
例如,以下操作可能被划分(编译器/硬件相关):
i += 1;
因为它可以被另一个线程(在假设的硬件和编译器上)观察为:
load r1, i;
addi r1, #1;
store i, r1;
执行上述操作i += 1
而没有适当同步的两个线程可能会产生错误的结果。最初说i=0
,线程T1
加载T1.r1 = 0
,线程T2
加载t2.r1 = 0
。两个线程将它们各自的r1
增加1,然后将结果存储到i
。虽然已经执行了两个增量,但i
的值仍然只有1,因为增量操作是可分的。请注意,如果在i+=1
之前和之后有同步,则另一个线程将等待操作完成,因此会观察到一个不可分割的操作。
请注意,即使是简单的写入也可以是不可分割的:
i = 3;
store i, #3;
取决于编译器和硬件。例如,如果i
的地址未正确对齐,则必须使用未对齐的加载/存储,由CPU作为几个较小的加载/存储执行。
非原子操作可能会重新排序,可能不一定按照程序源代码中的顺序发生。
例如,在"as-if" rule下,只要所有对易失性存储器的访问按程序指定的顺序“仿佛”程序发生,就允许编译器按其认为合适的方式重新排序存储和加载。根据标准中的措辞进行评估。因此,可以重新安排非原子操作,从而破坏关于多线程程序中的执行顺序的任何假设。这就是为什么在多线程编程中看似无害地使用原始int
作为信号变量的原因被打破,即使写入和读取可能是不可分割的,排序可能会根据编译器破坏程序。原子操作根据指定的内存语义强制执行对其周围操作的排序。请参阅std::memory_order
。
CPU也可能在该CPU的内存排序约束下重新排序内存访问。您可以在Intel 64 and IA32 Architectures Software Developer Manual部分8.2中找到x86架构的内存排序约束,从第2212页开始。
int
,char
等)不是Atomic 因为即使它们在某些条件下可能具有不可分割的存储和加载指令,甚至可能有一些算术指令,它们也不能保证存储和加载的顺序。因此,在没有适当同步的情况下在多线程上下文中使用它们是不安全的,以保证其他线程观察到的内存状态是您认为在那个时间点的内存状态。
我希望这能解释为什么原始类型不是原子的。
答案 3 :(得分:3)
到目前为止我在其他答案中没有提到的其他信息:
例如,如果您使用std::atomic<bool>
,并且bool
在目标体系结构上实际上是原子的,那么编译器将不会生成任何冗余的围栏或锁。将生成与普通bool
相同的代码。
换句话说,如果实际需要在平台上保持正确性,那么使用std::atomic
只会降低代码的效率。所以没有理由避免它。