C++:标准::原子<bool>和挥发性布尔值

C++ : std::atomic<bool> and volatile bool

本文关键字:挥发性 布尔值 gt lt 标准 原子 C++ bool      更新时间:2023-10-16

我正在阅读Anthony Williams的《C++并发操作手册》。有一个经典的例子,有两个线程,一个产生数据,另一个消耗数据,A.W.非常清楚地写下了代码:

std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread()
{
while(!data_ready.load())
{
std::this_thread::sleep(std::milliseconds(1));
}
std::cout << "The answer=" << data[0] << "n";
}
void writer_thread()
{
data.push_back(42);
data_ready = true;
}

我真的不明白为什么这段代码与我使用经典的volatile bool而不是原子bool的代码不同。如果有人能让我敞开心扉,我将不胜感激。谢谢

A"经典的";正如您所说,bool不会可靠地工作(如果有的话)。其中一个原因是编译器只能从内存中加载data_ready一次(而且很可能是这样,至少在启用了优化的情况下),因为没有迹象表明它在reader_thread的上下文中会发生变化。

您可以通过使用volatile bool强制每次加载它来解决这个问题(这可能看起来有效),但这仍然是C++标准中未定义的行为,因为对变量的访问既不是同步的,也不是原子的。

您可以使用互斥体头中的锁定功能来强制同步,但这会引入(在您的示例中)不必要的开销(因此std::atomic)。


volatile的问题在于,它只保证指令不被省略,并且保留指令顺序。volatile不能保证内存屏障来强制缓存一致性。这意味着处理器A上的writer_thread可以将值写入其缓存(甚至可能写入主内存),而处理器B上的reader_thread看不到它,因为处理器B的缓存与处理器A的缓存不一致。有关更全面的解释,请参阅维基百科上的内存屏障和缓存一致性。


x = y(即x += y)相比,更复杂的表达式可能存在额外的问题,需要通过锁(或者在这种简单的情况下是原子+=)进行同步,以确保x的值在处理过程中不会改变。

例如x += y实际上是:

  • 读取x
  • 计算x + y
  • 将结果写回x

如果在计算过程中发生上下文切换到另一个线程,这可能会导致类似的情况(两个线程都在执行x += 2;假设为x = 0):

Thread A                 Thread B
------------------------ ------------------------
read x (0)
compute x (0) + 2
<context switch>
read x (0)
compute x (0) + 2
write x (2)
<context switch>
write x (2)

现在CCD_ 20,尽管有两个CCD_。这种效应被称为撕裂

最大的区别是这个代码是正确的,而使用bool而不是atomic<bool>的版本有未定义的行为。

这两行代码创建了一个竞争条件(形式上是冲突),因为它们读取和写入同一个变量:

阅读器

while (!data_ready)

作家

data_ready = true;

根据C++11内存模型,正常变量上的竞争条件会导致未定义的行为。

规则见本标准第1.10节,最相关的是:

如果

  • 它们由不同的线程执行,或者
  • 它们是无序列的,并且至少有一个是由信号处理器执行的

如果程序的执行包含两个潜在的并发冲突操作,其中至少一个操作不是原子操作,并且两个操作都不在另一个操作之前发生,则程序的执行将包含数据竞赛,以下所述的信号处理程序的特殊情况除外。任何这样的数据竞赛都会导致未定义的行为。

您可以看到变量是否为atomic<bool>对该规则有很大的影响。

Ben Voigt的答案是完全正确的,仍然有点理论性,当一位同事问我"这对我意味着什么"时,我决定用一个更实用的答案来碰碰运气。

对于您的样本,可能出现的"最简单"的优化问题如下:

根据该标准,优化的执行顺序可能不会改变程序的功能。问题是,这仅适用于单线程程序或多线程程序中的单线程。

因此,对于writer_thread和(易失性)bool

data.push_back(42);
data_ready = true;

data_ready = true;
data.push_back(42);

是等效的。

结果是,

std::cout << "The answer=" << data[0] << "n";

可以在不将任何值推入数据的情况下执行。

原子布尔确实阻止了这种优化,根据定义,它可能不会被重新排序。原子操作有一些标志,允许语句移动到操作的前面,但不能移动到后面,反之亦然,但这些标志需要对编程结构及其可能导致的问题有非常深入的了解。。。