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

jed*_*dib 16 c++ multithreading

我只是在阅读Anthony Williams的行动书中的C++并发性.有这个经典的例子有两个线程,一个产生数据,另一个消耗数据,AW写的代码很清楚:

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;
}
Run Code Online (Sandbox Code Playgroud)

我真的不明白为什么这个代码与我使用经典的挥发性bool而不是原子的不同.如果有人能够对这个问题敞开心扉,我将不胜感激.谢谢.

Ben*_*igt 14

最大的区别是这段代码是正确的,而版本bool代替atomic<bool>具有未定义的行为.

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

读者

while (!data_ready)
Run Code Online (Sandbox Code Playgroud)

和作家

data_ready = true;
Run Code Online (Sandbox Code Playgroud)

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

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

如果有两个动作可能是并发的

  • 它们由不同的线程执行,或者
  • 它们没有排序,至少有一个是由信号处理程序执行的.

程序的执行包含数据竞争,如果它包含两个可能同时发生冲突的动作,其中至少有一个不是原子的,并且除了下面描述的信号处理程序的特殊情况之外,它们都不会发生在另一个之前.任何此类数据争用都会导致未定义的行为.

您可以看到变量是否atomic<bool>对此规则产生了很大的影响.

  • @jedib:发生未定义的行为,因为行为未定义.如果您要求在这种特殊情况下可能出现的错误行为的具体示例:没有内存屏障,读取线程可能会在更新向量视图之前看到`bool`更改.或者它可能根本看不到更改,在不保证缓存一致性的处理器上. (4认同)
  • 好的,有一个竞争条件,但是如果在存储区之前读取布尔值,最终结果将是另一轮while循环。对于未定义的行为,因为bool只能具有两种状态,即true或false。因此,当布尔变量设置为false时,一个正义者必须修改一点以使其成立。那么未定义的行为怎么会在这里发生。当存在竞争条件并且一个线程可以看到一个处于不一致状态的半状态对象时,将发生未定义的行为。因此,从我的角度来看,bool或std :: atomic &lt;bool&gt;在代码结果方面是相同的。 (2认同)

Max*_*uxa 8

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

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

您可以使用互斥锁头中的锁定工具强制执行同步,但这会(在您的示例中)引入不必要的开销(因此std::atomic).


问题volatile是它只保证不会省略指令并保留指令顺序.volatile 保证内存屏障可以强制执行缓存一致性.这意味着writer_thread处理器A可以在没有reader_thread处理器B看到它的情况下将值写入其缓存(甚至可能写入主存储器),因为处理器B的缓存与处理器A的缓存不一致.详细解释请参阅维基百科上的内存障碍缓存一致性.


然后可能存在更多"复杂"表达式的其他问题x = y(即x += y)需要通过锁定(或在这种简单情况下是原子+=)同步以确保x在处理期间值不会改变.

x += y 例如实际上是:

  • x
  • 计算 x + y
  • 把结果写回去 x

如果在计算过程中发生了上下文切换到另一个线程,这可能导致类似这样的事情(2个线程,两者都在做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)
Run Code Online (Sandbox Code Playgroud)

现在x = 2即使有两个+= 2计算.这种效应被称为撕裂.


小智 7

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

对于您的示例,可能发生的“最简单”优化问题如下:

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

因此,对于 writer_thread 和(易失性)布尔值

data.push_back(42);
data_ready = true;
Run Code Online (Sandbox Code Playgroud)

data_ready = true;
data.push_back(42);
Run Code Online (Sandbox Code Playgroud)

是等价的。

结果是,

std::cout << "The answer=" << data[0] << "\n";
Run Code Online (Sandbox Code Playgroud)

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

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