CMPXCHG 指令和 lwarx/stwcx(LL/SC)指令是在不同体系结构中常见的原子操作指令。
- CMPXCHG:
CMPXCHG是 x86 架构中用于执行原子比较并交换操作的指令。这个指令会比较某个内存位置的值与累加器中的值,如果相等,则将累加器中的值赋给该内存位置,并且返回旧的内存位置的值。因此,CMPXCHG通常用于实现原子的读-改-写操作。
- lwarx/stwcx (LL/SC):
lwarx/stwcx是 PowerPC 架构中用于实现类似原子操作的指令。lwarx用于加载一个字(word)的值到寄存器中,而stwcx用于尝试将新值存储回内存。如果在这次加载和存储之间没有其他处理器修改了这个内存位置,则存储操作成功,否则它失败。
ABA 问题:
- ABA 问题指的是一个共享内存区域中的值被修改两次,最终回到了原始的值,使得检查不到这个变化。这种情况可能会导致并发数据结构出现问题。
CMPXCHG指令在解决ABA问题上存在缺陷,因为它只能检查共享内存区域的当前值是否等于预期值,但无法检查这个值是否“曾经”等于预期值。这就是 ABA 问题的根源。- 相比之下,
lwarx/stwcx指令通过使用 LL/SC 的方式,可以在加载和存储之间检测共享内存区域的变化情况,因此可以避免 ABA 问题的发生。
因此,lwarx/stwcx 指令能够提供更强大的原子性保证,从而避免了类似于 ABA 问题的并发风险。
有一种比较有效的做法是:将共享内存区域的修改与版本号或标记相关联。每次对共享内存的修改都会增加版本号,这样即使出现了ABA问题,由于版本号已经发生了改变,便可检测到这种情况。
一些库或语言提供了支持带有回退机制的CAS操作,例如 Java 中的 AtomicStampedReference 类。该类允许用户为引用的目标对象附加一个整数作为版本戳,并且在CAS操作失败时,可以检查目标对象是否还包含相同的版本戳。
下面这个是C++实现的带版本号的CAS
一种常见的方法是创建一个结构体,其中包含指向数据的指针以及版本号或标记,并使用原子操作来更新和比较这两个值。通过使用 std::atomic 来确保这些操作是原子的。
#include <atomic>
template <typename T, typename U>
struct StampedReference {
T* pointer;
U stamp;
};
// 使用StamedReference进行CAS操作
template <typename T, typename U>
bool atomicCompareAndSet(std::atomic<StampedReference<T, U>>& ref, T* expectedPointer, T* newPointer, U expectedStamp, U newStamp) {
StampedReference<T, U> oldValue = ref.load();
if (oldValue.pointer == expectedPointer && oldValue.stamp == expectedStamp) {
StampedReference<T, U> newValue {newPointer, newStamp};
return ref.compare_exchange_weak(oldValue, newValue);
}
return false;
}
在上面的示例中,我们创建了一个 StampedReference 结构体来存储指针和版本号,并编写了一个模板函数 atomicCompareAndSet 来执行CAS操作。当CAS操作失败时,可以检查版本戳并采取适当的行动。
CAS(Compare and Swap)通常与循环结合使用,是因为在并发环境下,多个线程尝试对同一内存位置执行 CAS 操作时可能会遇到竞争条件。如果不使用循环来重试 CAS 操作,那么在竞争激烈的情况下,单次 CAS 操作失败后,程序可能就会放弃修改,这样会导致并发操作的数据一致性出现问题。
通过在循环中重试 CAS 操作,可以确保在 CAS 操作成功之前不会跳出循环,从而保证了原子性和一致性。这种方法被称为自旋锁,因为线程会持续尝试 CAS 直到成功,而不是将自己挂起等待。
当使用 atomicCompareAndSet 这样的原子操作函数时,通常还需要将其放置在一个循环中以确保 CAS 操作的原子性。这种自旋方式会在多线程环境中保证正确的行为,并且能够有效地避免线程切换所带来的开销。
总之,即使使用了原子操作函数,也仍然需要搭配使用循环,以确保 CAS 操作的成功和数据的一致性。
781

被折叠的 条评论
为什么被折叠?



