Multi-threaded executions and data races (since C++11)
一个 执行线程 是程序内部的控制流,起始于特定顶层函数的调用(通过 std::thread 、 std::async 、 std::jthread (C++20 起) 或其他方式),并递归地包含该线程后续执行的每个函数调用。
- 当一个线程创建另一个线程时,新线程顶层函数的初始调用是由新线程执行的,而非由创建线程执行。
任何线程都可能访问程序中的任意对象和函数:
- 具有自动和线程局部 存储期 的对象仍可能通过指针或引用被其他线程访问。
- 在 托管实现 下,C++程序可以拥有多个并发运行的线程。每个线程的执行遵循本页其余部分的定义。整个程序的执行由其所有线程的执行共同构成。
- 在 独立实现 下,程序是否能够拥有多个执行线程由实现定义。
对于非因调用 std::raise 而执行的 信号处理程序 ,信号处理程序调用发生在哪个执行线程中是由实现定义的。
目录 |
数据竞争
不同的执行线程始终被允许并发访问(读取和修改)不同的 内存位置 ,无需任何干涉且无需同步要求。
两个表达式 求值 过程若存在 冲突 ,是指其中一个表达式修改了某个内存位置,或开始/结束该内存位置中对象的生存期,而另一个表达式读取或修改了同一内存位置,或开始/结束占用与该内存位置存在重叠的存储空间的对象生存期。
一个具有两个冲突求值的程序存在 数据竞争 ,除非
- 两个求值操作在同一线程或同一 信号处理程序 中执行,或
- 两个冲突求值都是原子操作(参见 std::atomic ),或
- 其中一个冲突求值 先发生于 另一个(参见 std::memory_order )。
如果发生数据竞争,程序的行为是未定义的。
(特别地, std::mutex 的释放会与另一个线程对同一互斥量的获取形成 同步关系 ,因此前者 先发生于 后者,这使得利用互斥锁防止数据竞争成为可能。)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // 未定义行为
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // 正确
容器数据竞争
标准库中除
std
::
vector
<
bool
>
外的所有
容器
均保证:对同一容器中不同元素所含对象的并发修改永远不会导致数据竞争。
std::vector<int> vec = {1, 2, 3, 4}; auto f = [&](int index) { vec[index] = 5; }; std::thread t1{f, 0}, t2{f, 1}; // 正确 std::thread t3{f, 2}, t4{f, 2}; // 未定义行为
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // 未定义行为
内存顺序
当线程从内存位置读取值时,它可能看到初始值、同一线程写入的值或其他线程写入的值。关于线程间写入操作对其他线程可见的顺序细节,请参阅 std::memory_order 。
前进保证
Obstruction freedom
当仅有一个未阻塞在标准库函数中的线程执行 原子操作函数 且该函数是无锁操作时,该执行保证能够完成(所有标准库无锁操作都是 无阻碍的 )。
无锁性
当一个或多个无锁原子函数并发执行时,至少有一个操作保证能够完成(所有标准库无锁操作都是 无锁的 — 具体实现需要确保这些操作不会被其他线程无限期地活锁,例如通过持续抢占缓存行)。
进度保证
在有效的 C++ 程序中,每个线程最终会执行以下操作之一:
- 终止。
- 调用 std::this_thread::yield 。
- 调用库I/O函数。
- 通过 volatile 泛左值执行访问。
- 执行原子操作或同步操作。
- 继续执行平凡无限循环(见下文)。
当线程执行上述任一执行步骤、在标准库函数中阻塞,或调用因非阻塞并发线程而未能完成的无锁原子操作时,该线程即被称为 取得进展 。
这使得编译器能够移除、合并和重排所有没有可观察行为的循环,而无需证明它们最终会终止,因为它可以假设没有执行线程能够永远执行而不执行任何这些可观察行为。对于无法被移除或重排的简单无限循环,则提供了特殊处理机制。
平凡无限循环
一个 平凡空迭代语句 是符合以下形式之一的迭代语句:
while (
条件
) ;
|
(1) | ||||||||
while (
条件
) { }
|
(2) | ||||||||
do ; while (
条件
) ;
|
(3) | ||||||||
do { } while (
条件
) ;
|
(4) | ||||||||
for (
初始化语句 条件
(可选)
; ) ;
|
(5) | ||||||||
for (
初始化语句 条件
(可选)
; ) { }
|
(6) | ||||||||
一个平凡空迭代语句的 控制表达式 是:
一个 平凡无限循环 是指当 显式常量求值 时,其转换后的控制表达式为 常量表达式 且求值结果为 true 的平凡空迭代语句。
简单无限循环的循环体会被替换为对函数 std::this_thread::yield 的调用。此替换是否在 独立实现 中发生由实现定义。
for (;;); // 简单的无限循环,根据P2809标准已明确定义 for (;;) { int x; } // 未定义行为
并发向前进展若线程提供 并发向前进展保证 ,则只要该线程尚未终止,无论其他线程(若存在)是否在取得进展,它都将在有限时间内 取得进展 (定义如上)。 标准鼓励但不要求主线程及由 std::thread 和 std::jthread (C++20 起) 启动的线程提供并发向前进展保证。 并行向前进展若线程提供 并行向前进展保证 ,则实现无需确保该线程在尚未执行任何执行步骤(I/O、volatile、原子或同步操作)时最终会取得进展,但一旦该线程执行了步骤,它即提供 并发向前进展 保证(此规则描述了线程池中以任意顺序执行任务的线程)。 弱并行向前进展若线程提供 弱并行向前进展保证 ,则不保证其最终会取得进展,无论其他线程是否取得进展。
此类线程仍可通过带向前进展委托的阻塞来保证取得进展:若线程
C++标准库中的 并行算法 会在由库管理的未指定线程组完成时,通过向前进展委托进行阻塞。 |
(C++17 起) |
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时行为 | 正确行为 |
|---|---|---|---|
| CWG 1953 | C++11 |
开始/结束具有重叠存储对象的生命周期的
两个表达式求值不构成冲突 |
构成冲突 |
| LWG 2200 | C++11 |
未明确容器数据竞争要求
是否仅适用于序列容器 |
适用于所有容器 |
| P2809R3 | C++11 |
执行“平凡”
[1]
无限循环的行为未定义 |
正确定义“平凡无限循环”
并使行为明确定义 |
- ↑ 此处的“平凡”指执行无限循环永远不会产生任何进展。