比较和交换循环是如何实现原子性的



本页详细介绍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;
}

因为在loadstore之间,另一个线程可以修改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。

最新更新