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

Are C/C++ fundamental types atomic?

是 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。我很确定该标准不是免费的,因此这不是 final/official 版本。

1.10 多线程执行和数据竞争

  1. Two expression evaluations conflict if one of them modifies a memory location (1.7) and the other one reads or modifies the same memory location.
  2. The library defines a number of atomic operations (Clause 29) and operations on mutexes (Clause 30) that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another. A synchronization operation on one or more memory locations is either a consume operation, an acquire operation, a release operation, or both an acquire and release operation. A synchronization operation without an associated memory location is a fence and can be either an acquire fence, a release fence, or both an acquire and release fence. In addition, there are relaxed atomic operations, which are not synchronization operations, and atomic read-modify-write operations, which have special characteristics.


  1. Two actions are potentially concurrent if
    (23.1) — they are performed by different threads, or
    (23.2) — they are unsequenced, and at least one is performed by a signal handler.
    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

29.5 原子类型

  1. There shall be explicit specializations of the atomic template for the integral types ``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, and any other types needed by the typedefs in the header <cstdint>. For each integral type integral, the specialization atomic<integral> provides additional atomic operations appropriate to integral types. There shall be a specialization atomic<bool> which provides the general atomic operations as specified in 29.6.1..


  1. There shall be pointer partial specializations of the atomic class template. These specializations shall have standard layout, trivial default constructors, and trivial destructors. They shall each support aggregate initialization syntax.

29.7 标志类型和操作

  1. Operations on an object of type atomic_flag shall be lock-free. [ Note: Hence the operations should also be address-free. No other type requires lock-free operations, so the atomic_flag type is the minimum hardware-implemented type needed to conform to this International standard. The remaining types can be emulated with atomic_flag, though with less than ideal properties. — end note ]

由于尽管 C 不在标签中,但问题中(当前)也提到了 C,因此 C Standard 指出:

5.1.2.3 Program execution

...

When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects that are neither lock-free atomic objects nor of type volatile sig_atomic_t are unspecified, as is the state of the floating-point environment. The value of any object modified by the handler that is neither a lock-free atomic object nor of type volatile sig_atomic_t becomes indeterminate when the handler exits, as does the state of the floating-point environment if it is modified by the handler and not restored to its original state.

5.1.2.4 Multi-threaded executions and data races

...

Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.

[several pages of standards - some paragraphs explicitly addressing atomic types]

The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

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

什么是原子?

原子的,用原子的 属性 来描述某物。原子一词源自拉丁语atomus,意思是“不可分割的”。

通常我认为原子操作(不考虑语言)具有两个特性:

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

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

例如下面的操作可能被划分(compiler/hardware依赖):

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 的地址未适当对齐,则必须使用未对齐的 load/store,它由 CPU 执行为几个较小的 loads/stores.

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

非原子操作可能会重新排序,不一定按照程序源代码中写入的顺序发生。

例如"as-if" rule the compiler is allowed to re-order stores and loads as it sees fit as long as all access to volatile memory occurs in the order specified by the program "as if" the program was evaluated according to the wording in the standard. Thus non-atomic operations may be re-arranged breaking any assumptions about execution order in a multi-threaded program. This is why a seemingly innocent use of a raw int as a signaling variable in multi-threaded programming is broken, even if writes and reads may be indivisible, the ordering may break the program depending on the compiler. An atomic operation enforces ordering of the operations around it depending on what memory semantics are specified. See std::memory_order.

CPU 也可能会根据 CPU 的内存排序约束重新排序您的内存访问。您可以在从第 2212 页开始的 Intel 64 and IA32 Architectures Software Developer Manual 部分 8.2 中找到 x86 架构的内存排序约束。

原始类型(intchar 等)不是原子类型

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

我希望这能解释 为什么 基本类型不是原子的。

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

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

相同的代码

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