命名空间
变体
操作

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

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

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

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

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

  • 具有自动和线程本地 存储期限 的对象仍然可以通过指针或引用被其他线程访问。
  • 托管实现 下,C++ 程序可以有多个线程同时运行。每个线程的执行按照本页其余部分的定义进行。整个程序的执行包含其所有线程的执行。
  • 独立实现 下,程序是否可以有多个执行线程是实现定义的。

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

内容

[编辑] 数据竞争

不同的执行线程总是被允许同时访问 (读取和修改) 不同的 内存位置,而不会相互干扰,也不需要同步。

当表达式的 求值 修改了内存位置,而另一个求值读取或修改了同一个内存位置时,则这两个表达式被称为冲突。如果程序存在两个冲突的求值,则除非

  • 这两个求值都在同一个线程或同一个 信号处理程序 中执行,或者
  • 这两个冲突的求值都是原子操作 (参见 std::atomic),或者
  • 其中一个冲突的求值发生在另一个求值之前 (参见 std::memory_order),

否则该程序就存在数据竞争

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

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::mutex 与另一个线程获取同一个互斥锁同步,因此发生在另一个线程获取同一个互斥锁之前,这使得可以使用互斥锁来防止数据竞争。)

[编辑] 内存顺序

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

[编辑] 向前进展

当且仅当一个线程在标准库函数中没有阻塞,并且执行了一个无锁的原子函数,则该执行保证完成(所有标准库无锁操作都是无阻塞的)。

[edit] 无锁

当一个或多个无锁原子函数同时运行时,至少保证其中一个函数完成(所有标准库无锁操作都是无锁的 - 实现需要保证它们不能被其他线程无限期地活锁,例如,通过不断地窃取缓存行)。

[edit] 进度保证

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

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

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

这允许编译器删除、合并和重新排序所有没有可观察行为的循环,而无需证明它们最终会终止,因为编译器可以假定,没有执行线程可以永远执行而无需执行任何这些可观察的行为。简单的无限循环是一种例外情况,不能被删除或重新排序。

[edit] 简单的无限循环

一个简单的空迭代语句是一个匹配以下形式之一的迭代语句

while ( condition ) ; (1)
while ( condition ) { } (2)
do ; while ( condition ) ; (3)
do { } while ( condition ) ; (4)
for ( init-statement condition (optional) ; ) ; (5)
for ( init-statement condition (optional) ; ) { } (6)
1) 一个while 语句,其循环体是一个空的简单语句。
2) 一个while 语句,其循环体是一个空的复合语句。
3) 一个do-while 语句,其循环体是一个空的简单语句。
4) 一个do-while 语句,其循环体是一个空的复合语句。
5) 一个for 语句,其循环体是一个空的简单语句,for 语句没有迭代表达式
6) 一个for 语句,其循环体是一个空的复合语句,for 语句没有迭代表达式

一个简单的空迭代语句的控制表达式

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启动的线程提供并发向前进度保证。

并行向前进度

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

弱并行向前进度

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

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

来自 C++ 标准库的并行算法使用向前进度保证委托进行阻塞,在某组未指定的库管理线程完成时进行阻塞。

(自 C++17)

[edit] 缺陷报告

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

DR 应用于 已发布的行为 正确行为
P2809R3 C++11 执行“简单的”[1]
无限循环的行为是未定义的
正确定义了“简单的无限循环”
并使行为成为定义明确的
  1. “简单”在这里意味着执行无限循环永远不会取得任何进展。