本页详细介绍CAS循环:https://preshing.com/20150402/you-can-do-any-kind-of-atomic-read-modify-write-operation/
C++中fetch_multiply
的一个例子:
uint32_t fetch_multiply(std::atomic<uint32_t>& shared, uint32_t multiplier){
uint32_t oldValue = shared.load();
while (!shared.compare_exchange_weak(oldValue, oldValue * multiplier)){}
return oldValue;
}
本质上,如果*memory
的值与我们的oldValue匹配,那么newValue
将被原子存储,否则oldValue将用*memory
更新。
我有两个问题:
1-为什么我们必须检查oldValue
在内存中是否仍然不变?如果我们只是将newValue写入内存,会发生什么?我们是否试图避免覆盖或使用来自另一个线程的中间值?
2-假设这个场景有2个线程:
- 线程B正试图以非原子方式存储未对齐的值。发生商店撕裂
- 线程A尝试交换
- 交换失败,因为oldValue不匹配。内存中的中间值(teared(将加载到我们的oldValue中
- 线程A与中间值进行乘法运算,并尝试另一个成功的交换
- 现在线程B将其剩余的值写入到相同的位置,部分覆盖了我们之前的写入
我假设Thread B
可以在这么多延迟的情况下运行,如果是这样的话乘以一个中间值,它甚至得到了部分重写的后记,CAS什么也没做。
如果目标值更改为其他值,我们需要再次读取oldValue,否则我们将永远旋转。
然而,CAS构造的要点是,您永远无法在共享位置中观察到中间值。眼泪是不可能的;shared.load()
阻止了它。这是在硬件中实现的。
"如果我们只是将newValue写入内存,会发生什么"那么你就没有原子访问权限了。始终遵循模式。
"非对齐值";如果shared
是不对齐的,那么您甚至在讨论std::atomic
之前就已经在代码中引入了未定义的行为。无法安全地取消引用未对齐的指针。对于普通的*
,您只是依赖于字节可寻址的体系结构,但这是一个std::atomic
。如果它没有对齐,您甚至可以在x86上出错。
为什么这不起作用?
uint32_t fetch_multiply(std::atomic<uint32_t>& shared, uint32_t multiplier){
uint32_t oldValue = shared.load();
uint32_t newValue = oldValue * multiplier;
shared.store(newValue);
return oldValue;
}
因为在load
和store
之间,另一个线程可以修改shared
的值。
考虑一下这个问题:
std::atomic<uint32_t> shared{1};
std::thread t1{ fetch_multiply, std::ref(shared), 2 };
std::thread t2{ fetch_multiply, std::ref(shared), 2 };
t1.join();
t2.join();
std::cout << shared;
通过以上实现,该程序的可能输出为2。而正确的一个(如果fetch_multiply
应该同步(必须是4。当两个线程都首次加载初始值1时,就会出现问题。然后,它们都存储它们的本地结果2。