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

How does a compare and swap loop achieve atomicity?

此页面详细讨论了 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 个线程:

我假设 Thread B 可以延迟那么久,如果是这样的话,不仅我们 乘以一个中间值,它甚至被部分覆盖后记,而 CAS 什么也没做。

如果目标值更改为其他值,我们需要再次读取旧值,否则我们将永远旋转。

然而,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.