命名空间
变体
操作

多线程执行与数据竞争 (C++11 起)

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

一个 执行线程 是程序内的一个控制流,始于特定顶层函数的调用(通过 std::threadstd::asyncstd::jthread(C++20 起) 或其他方式),并递归地包含随后由该线程执行的每个函数调用。

  • 当一个线程创建另一个线程时,新线程的顶层函数的初始调用由新线程执行,而不是由创建线程执行。

任何线程都可以潜在地访问程序中的任何对象和函数

  • 具有自动和线程局部存储期的对象仍可以通过指针或引用被其他线程访问。
  • 宿主实现下,C++ 程序可以有多个线程并发运行。每个线程的执行按本页其余部分定义进行。整个程序的执行由其所有线程的执行组成。
  • 独立实现下,程序是否可以有多个执行线程是实现定义的。

对于并非因调用 std::raise 而执行的信号处理程序,包含信号处理程序调用的执行线程是未指定的。

目录

[编辑] 数据竞争

不同的执行线程总是被允许并发访问(读取和修改)不同的内存位置,没有干扰,也没有同步要求。

如果两个表达式求值中,一个修改了内存位置或开始/结束了内存位置中对象的生命周期,而另一个读取或修改了相同的内存位置或开始/结束了占据该内存位置存储的对象的生命周期,则这两个表达式求值就 冲突

一个程序,如果存在两个冲突的求值,则存在 数据竞争,除非:

如果发生数据竞争,程序的行为是未定义的。

(特别是,std::mutex 的释放 synchronized-with(同步于),因此 happens-before(先行于)另一个线程对同一互斥体的获取,这使得可以使用互斥锁来防止数据竞争。)

int cnt = 0;
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // undefined behavior
std::atomic<int> cnt{0};
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // OK

[编辑] 容器数据竞争

标准库中除了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}; // OK
std::thread t3{f, 2}, t4{f, 2}; // undefined behavior
std::vector<bool> vec = {false, false};
auto f = [&](int index) { vec[index] = true; };
std::thread t1{f, 0}, t2{f, 1}; // undefined behavior

[编辑] 内存顺序

当一个线程从内存位置读取一个值时,它可能会看到初始值、在同一线程中写入的值,或在另一个线程中写入的值。有关从线程进行的写入对其他线程可见的顺序的详细信息,请参阅 std::memory_order

[编辑] 向前进展

[编辑] 无阻塞性

当只有一个线程(未被标准库函数阻塞)执行无锁的原子函数时,该执行保证完成(所有标准库无锁操作都具有无阻塞性)。

[编辑] 无锁性

当一个或多个无锁原子函数并发运行时,至少有一个函数保证完成(所有标准库无锁操作都具有无锁性 —— 确保它们不会被其他线程无限地活锁,例如通过持续窃取缓存行,是实现的工作)。

[编辑] 进展保证

在一个有效的 C++ 程序中,每个线程最终都会执行以下操作之一:

  • 终止。
  • 调用 std::this_thread::yield
  • 调用库 I/O 函数。
  • 通过 volatile glvalue 进行访问。
  • 执行原子操作或同步操作。
  • 继续执行平凡无限循环(见下文)。

如果一个线程执行了上述执行步骤之一,在标准库函数中阻塞,或者调用了一个原子无锁函数但因未阻塞的并发线程而未能完成,则称该线程 取得了进展

这允许编译器移除、合并和重排所有没有可观察行为的循环,而无需证明它们最终会终止,因为它可以假定没有任何执行线程可以永远执行而不执行任何这些可观察行为。对平凡无限循环做出了例外,它们不能被移除或重排。

[编辑] 平凡无限循环

一个 平凡空迭代语句 是符合以下形式之一的迭代语句:

while ( 条件 ) ; (1)
while ( 条件 ) { } (2)
do ; while ( 条件 ) ; (3)
do { } while ( 条件 ) ; (4)
for ( init-statement 条件 (可选) ; ) ; (5)
for ( init-statement 条件 (可选) ; ) { } (6)
1) 循环体为空简单语句的 while 语句
2) 循环体为空复合语句的 while 语句
3) 循环体为空简单语句的 do-while 语句
4) 循环体为空复合语句的 do-while 语句
5) 循环体为空简单语句,且 for 语句没有 iteration-expressionfor 语句
6) 循环体为空复合语句,且 for 语句没有 iteration-expressionfor 语句

一个平凡空迭代语句的 控制表达式 是:

1-4) 条件
5,6) 如果存在则为 条件,否则为 true

一个 平凡无限循环 是一个平凡空迭代语句,其转换后的控制表达式是常量表达式,当显式常量求值时,其值为 true

平凡无限循环的循环体被替换为对函数 std::this_thread::yield 的调用。在独立实现中是否发生此替换是实现定义的。

for (;;); // trivial infinite loop, well defined as of P2809
for (;;) { int x; } // undefined behavior

并发向前进展

如果一个线程提供 并发向前进展保证,则它在未终止的情况下,将在有限时间内 取得进展(如上定义),无论其他线程(如果有的话)是否取得进展。

标准鼓励但不要求主线程和由 std::thread std::jthread(C++20 起) 启动的线程提供并发向前进展保证。

并行向前进展

如果一个线程提供 并行向前进展保证,则实现不要求确保该线程在尚未执行任何执行步骤(I/O、volatile、原子或同步)的情况下最终会取得进展,但一旦该线程执行了一个步骤,它就会提供 并发向前进展 保证(此规则描述了线程池中以任意顺序执行任务的线程)。

弱并行向前进展

如果一个线程提供 弱并行向前进展保证,则它不保证最终会取得进展,无论其他线程是否取得进展。

这样的线程仍然可以通过阻塞并委托向前进展保证来确保取得进展:如果线程 P 以这种方式阻塞在线程集合 S 的完成上,那么 S 中至少一个线程将提供与 P 相同或更强的向前进展保证。一旦该线程完成,S 中另一个线程将以类似方式得到加强。一旦集合为空,P 将解除阻塞。

C++ 标准库中的并行算法通过委托向前进展保证来阻塞在未指定的一组库管理线程的完成上。

(C++17 起)

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 发布时的行为 正确的行为
CWG 1953 C++11 两个表达式求值,它们开始/结束生命周期
的重叠存储对象不冲突
它们冲突
LWG 2200 C++11 不清楚容器数据竞争
要求是否仅适用于序列容器
适用于所有容器
P2809R3 C++11 执行“平凡”[1]
无限循环的行为是未定义的
正确定义了“平凡无限循环”
并使行为变为良好定义
  1. 这里的“平凡”是指执行无限循环永远不会取得任何进展。