假设我们有一个内存区域,其中一些线程正在写入数据。然后它将注意力转移到其他地方并允许任意其他线程读取数据。但是,在某个时间点,它希望重用该内存区域并再次写入它。
写入程序线程提供一个布尔标志(valid
),表示内存仍然有效以便读取(即他还没有重用它)。在某些时候,他会将此标志设置为false,并且再也不会将其设置为true(它只会翻转一次,就是这样)。
对于顺序一致性,分别对作者和读者使用这两个代码片段应该是正确的:
...
valid = false;
<write to shared memory>
...
和
...
<read from shared memory>
if (valid) {
<be happy and work with data read>
} else {
<be sad and do something else>
}
...
我们显然需要做一些事情来确保顺序一致性,即插入必要的获取和释放内存障碍。我们希望在编写器线程中将标志设置为false,之前将任何数据写入段。我们希望在检查valid
之前,在读取器线程中从内存中读取数据。后者因为我们知道单调是有效的,即如果>> 之后它仍然有效,那么它在阅读时是有效的。
在内存访问和valid
访问之间插入一个完整的范围就可以了。但是,我想知道将valid
作为一个原子就足够了吗?
std::atomic<bool> valid = true;
然后
...
valid.store(false); // RELEASE
<write to shared memory>
...
和
...
<read from shared memory>
if (valid.load()) { // ACQUIRE
<be happy and work with data read>
} else {
<be sad and do something else>
}
...
在这种情况下,似乎隐含的释放和获取操作使用原子存储和读取工作对我不利。编写器中的RELEASE确实不阻止内存访问向上移动(只是上面的代码可能不会向下移动)。类似地,读者中的ACQUIRE确实不阻止内存访问向下移动(只是下面的代码可能无法向上移动)。
如果这是真的,为了使这个场景工作,我还需要在编写器线程中有一个ACQUIRE(即一个加载)和一个读取器线程中的RELEASE(即存储)。或者,我可以使用普通的布尔标志,并保护带有共享互斥锁的线程中的写入和读取访问权限(仅限于它!)。通过这样做,我有效地在两个线程中同时具有ACQUIRE和RELEASE,将valid
访问与内存访问分开。
因此atomic<bool>
与受bool
保护的常规mutex
之间存在非常严重的差异,这是正确的吗?
编辑:对于原子上的加载和存储,隐含似乎存在差异。 C ++ 11的std::atomic
分别使用memory_order_seq_cst
(!)而不是memory_order_acquire
和memory_order_release
来加载和存储。
相反,tbb::atomic
使用memory_semantics::acquire
和memory_semantics::release
而不是memory_semantics::full_fence
。
因此,如果我的理解是正确的,那么使用标准的C ++ 11原子代码是正确的,但是对于tbb原子,需要将显式memory_semantics::full_fence
模板参数添加到加载和存储。
答案 0 :(得分:1)
编写器将valid
标志切换为false
并开始写入数据,而读者可能仍在从中读取数据。
设计缺陷是错误的假设,只要读者在读完数据后检查数据有效性,读取和写入同一存储区就不是问题。
C ++标准将其称为数据竞争,并导致未定义的行为。
正确的解决方案是使用std::shared_mutex
来管理对单个作者和多个读者的访问。