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

Are C/C++ fundamental types atomic?

本文关键字:原子的 类型 C++      更新时间:2023-10-16

C/C++的基本类型,如intdouble等,是原子的,例如线程安全吗?

它们是否没有数据争用;也就是说,如果一个线程写入此类对象,而另一个线程从中读取,则行为是否定义良好?

如果不是,它取决于编译器还是其他东西?

不,基本数据类型(例如,intdouble(不是原子的,请参阅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. 对于整型 ''char、signed charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longchar16_ t、char32_twchar_t 以及标头<cstdint>中的 typedefs 所需的任何其他类型,原子模板应有明确的专用化。对于每个整型积分,专用化atomic<integral>提供适用于整型的附加原子操作。应有一个专门化atomic<bool>,提供29.6.1中规定的一般原子操作。


  1. 原子类模板应有指针部分专用化。这些专用化应具有标准布局、简单的默认构造函数和简单的析构函数。它们应各自支持聚合初始化语法。

29.7 标志类型和操作

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

由于 C 也(目前(在问题中被提及,尽管不在标签中,C 标准指出:

5.1.2.3 程序执行

当抽象机器的处理因接收而中断时 信号,既不是无锁原子的对象的值 未指定 volatile sig_atomic_t 类型的对象,以及 浮点环境的状态。任何对象的值 由既不是无锁原子对象也不是无锁原子对象的处理程序修改 类型volatile sig_atomic_t在处理程序时变得不确定 退出,浮点环境的状态(如果为 由处理程序修改,但未还原到其原始状态。

5.1.2.4 多线程执行和数据竞赛

两种表达计算 如果其中一个修改内存位置,而另一个读取或修改相同的内存位置,则发生冲突

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

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

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

什么是原子?

原子

,描述具有原子性质的事物。原子这个词起源于拉丁语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 s 递增 1,然后将结果存储到 i 。尽管已执行了两个增量,但由于增量操作是可分割的,因此 i 的值仍然仅为 1。请注意,如果在i+=1之前和之后进行同步,则另一个线程将等到操作完成,从而观察到一个未分割的操作。

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

i = 3;
store i, #3;

取决于编译器和硬件。例如,如果i的地址没有正确对齐,则必须使用未对齐的加载/存储,该加载/存储由CPU作为几个较小的加载/存储执行。

原子操作具有保证内存排序语义。

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

例如,在"as-if"规则下,只要对易失性存储器的所有访问都按照程序指定的顺序进行,编译器就可以按照它认为合适的方式重新排序存储和加载。因此,非原子操作可以重新排列,打破多线程程序中关于执行顺序的任何假设。这就是为什么在多线程编程中看似无辜地使用原始int作为信令变量被破坏的原因,即使写入和读取可能是不可分割的,排序也可能根据编译器破坏程序。原子操作根据指定的内存语义强制对其周围的操作进行排序。请参阅std::memory_order

CPU

还可以根据该 CPU 的内存排序约束对您的内存访问进行重新排序。您可以在英特尔 64 和 IA32 架构软件开发人员手册第 8.2 节中找到 x86 架构的内存排序约束,从第 2212 页开始。

基元类型(intchar等(不是原子的

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

我希望这可以解释为什么基元类型不是原子的。

到目前为止,

我还没有在其他答案中看到的其他信息:

例如,如果您使用 std::atomic<bool>,并且bool实际上是目标体系结构上的原子,则编译器不会生成任何冗余围栏或锁。将生成与普通bool相同的代码。

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