我正在阅读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;
}
而且我真的不明白为什么这个代码与我使用经典的挥发性bool而不是原子的bool不同。 如果有人能够对这个问题敞开心扉,我会感激不尽。 感谢。
答案 0 :(得分:13)
最大的区别在于此代码是正确的,而bool
而不是atomic<bool>
的版本具有未定义的行为。
这两行代码会创建竞争条件(正式地说,冲突),因为它们读取和写入同一个变量:
阅读器
while (!data_ready)
作家
data_ready = true;
根据C ++ 11内存模型,正常变量的竞争条件会导致未定义的行为。
规则见本标准第1.10节,最相关的是:
如果
,两个动作可能会并发
- 它们由不同的线程执行,或
- 它们没有排序,至少有一个是由信号处理程序执行的。
程序的执行包含数据竞争,如果它包含两个可能同时发生冲突的动作,其中至少有一个不是原子的,并且除了下面描述的信号处理程序的特殊情况之外,它们都不会发生在另一个之前。任何此类数据争用都会导致未定义的行为。
您可以看到变量是否为atomic<bool>
对此规则产生了很大的影响。
答案 1 :(得分:8)
A&#34;经典&#34;正如你所说,bool
无法可靠地运作(如果有的话)。这样做的一个原因是编译器可能(并且很可能,至少在启用了优化的情况下)仅从内存加载data_ready
,因为没有迹象表明它在reader_thread
的上下文中发生了变化
你可以通过使用volatile bool
强制每次加载它来解决这个问题(这看起来似乎有效)但是这仍然是关于C ++标准的未定义行为,因为对变量的访问既不是同步的也不是原子的。
您可以使用mutex header中的锁定工具强制执行同步,但这会(在您的示例中)引入不必要的开销(因此std::atomic
)。
volatile
的问题在于它只保证不会省略指令并保留指令顺序。 volatile
不保证内存屏障以强制执行缓存一致性。这意味着处理器A上的writer_thread
可以将值写入其缓存(甚至可能是主存储器)而处理器B上没有reader_thread
看到它,因为处理器的缓存B与处理器A的缓存不一致。有关更详细的说明,请参阅维基百科上的memory barrier和cache coherence。
更多&#34;复杂&#34;可能会有其他问题。表达式然后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)
现在x = 2
即使有两个+= 2
次计算。此效果称为撕裂。
答案 2 :(得分:1)
Ben Voigt的回答是完全正确的,仍然有点理论化,并且我已经被一位同事问过了,这对我来说意味着什么&#34;我决定尝试一下运气好一点实际答案。
使用您的样本,&#34;最简单的&#34;可能出现的优化问题如下:
根据标准,优化的执行顺序可能不会改变程序的功能。问题是,仅适用于单线程程序,或多线程程序中的单线程。
所以,对于writer_thread和一个(volatile)bool
data.push_back(42);
data_ready = true;
和
data_ready = true;
data.push_back(42);
是等价的。
结果就是那个
std::cout << "The answer=" << data[0] << "\n";
可以在不将任何值推入数据的情况下执行。
原子bool确实阻止了这种优化,根据定义它可能不会被重新排序。有原子操作的标志允许语句在操作前移动而不是在后面移动,反之亦然,但那些需要非常高级的编程结构知识及其可能导致的问题...