在回答这个问题时,出现了一个关于 OP 情况的进一步问题,我不确定:这主要是一个处理器架构问题,但也有一个关于 C++ 11 内存模型的连锁问题。
基本上,由于以下代码(为简单起见稍作修改),OP 的代码在更高的优化级别无限循环:
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
__sync_val_compare_and_swap()
GCC 的原子 CAS 内置在哪里。bits_ & mask
在进入循环之前检测到的情况下,GCC(合理地)将其优化为无限循环,true
完全跳过 CAS 操作,因此我建议进行以下更改(可行):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
__sync_synchronize();
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
在我回答之后,OP 指出更改bits_
为volatile uint8_t
似乎也有效。我建议不要走那条路,因为volatile
通常不应该用于同步,而且在这里使用栅栏似乎没有太大的缺点。
但是,我想得更多,在这种情况下,语义是这样的,即ov & MASK
检查是否基于陈旧值并不重要,只要它不是基于无限期陈旧的值(即只要循环最终被打破),因为实际的更新尝试bits_
是同步的。那么,对于任何现有的处理器,如果由另一个线程更新,那么volatile
这里足以保证这个循环最终终止吗?换句话说,在没有显式内存栅栏的情况下,实际上是否可以无限期地由处理器有效地优化未由编译器优化的读取?(编辑:bits_
bits_ & MASK == false
为了清楚起见,我在这里询问现代硬件实际上可能会做什么,因为假设读取是由编译器在循环中发出的,所以这在技术上不是一个语言问题,尽管用 C++ 语义表达它很方便。)
这是它的硬件角度,但要稍微更新它并使其也成为有关 C++11 内存模型的可回答问题,请考虑对上述代码进行以下变体:
// bits_ is "std::atomic<unsigned char>"
unsigned char ov = bits_.load(std::memory_order_relaxed);
while (true) {
if (ov & MASK) {
ov = bits_.load(std::memory_order_relaxed);
continue;
}
// compare_exchange_weak also updates ov if the exchange fails
if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
break;
}
}
cppreference声称这std::memory_order_relaxed
意味着“对围绕原子变量的内存访问的重新排序没有限制”,因此独立于实际硬件会或不会做什么,确实意味着在符合实现的另一个线程上更新后,从bits_.load(std::memory_order_relaxed)
技术上讲永远不会读取更新的值bits_
?
编辑:我在标准(29.4 p13)中找到了这个:
实现应该使原子存储在合理的时间内对原子负载可见。
因此,显然等待更新值“无限长”(大部分?)是不可能的,但是除了应该“合理”之外,没有任何具体的新鲜时间间隔的硬性保证;尽管如此,关于实际硬件行为的问题仍然存在。