命名空间
变体
操作

多线程执行和数据竞争 (since C++11)

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
for
range-for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (在 C++17* 中弃用)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 

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

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

任何线程都可能访问程序中的任何对象和函数

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

对于不是由于调用 std::raise 而执行的信号处理程序,未指定哪个执行线程包含信号处理程序调用。

目录

[编辑] 数据竞争

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

如果两个表达式求值之一修改了内存位置或开始/结束了内存位置中对象的生命周期,而另一个读取或修改了相同的内存位置或开始/结束了占用与该内存位置重叠的存储空间的对象生命周期,则它们冲突

除非满足以下条件,否则具有两个冲突求值的程序会发生数据竞争

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

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

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 左值执行访问。
  • 执行原子操作或同步操作。
  • 继续执行平凡无限循环(见下文)。

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

这允许编译器删除、合并和重新排序所有没有可观察行为的循环,而无需证明它们最终会终止,因为它可以假设没有执行线程可以在不执行任何这些可观察行为的情况下永远执行。为平凡无限循环提供了便利,平凡无限循环不能被删除或重新排序。

[编辑] 平凡无限循环

平凡空迭代语句是匹配以下形式之一的迭代语句

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

平凡空迭代语句的控制表达式

1-4) condition
5,6) condition (如果存在),否则为 true

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

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

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

并发前向进度

如果线程提供并发前向进度保证,只要它没有终止,无论其他线程(如果有)是否取得进展,它都会在有限的时间内取得进展(如上定义)。

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

并行前向进度

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

弱并行前向进度

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

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

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

(since C++17)

[编辑] 缺陷报告

以下行为变更缺陷报告被追溯应用于以前发布的 C++ 标准。

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