比较和交换循环如何实现原子性?
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 个线程:
- 线程 B 正在尝试以非原子方式存储非对齐值。发生店铺撕裂。
- 线程 A 尝试交换。
- 交换失败,因为旧值不匹配。来自内存的 中间(撕裂)值被加载到我们的旧值。
- 线程 A 与 中间值 相乘并尝试另一个成功的交换。
- 现在线程 B 将其剩余的值写入相同的位置,部分覆盖我们之前的写入。
我假设 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;
}
因为在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.
此页面详细讨论了 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 尝试交换。
- 交换失败,因为旧值不匹配。来自内存的 中间(撕裂)值被加载到我们的旧值。
- 线程 A 与 中间值 相乘并尝试另一个成功的交换。
- 现在线程 B 将其剩余的值写入相同的位置,部分覆盖我们之前的写入。
我假设 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;
}
因为在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.