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 操作的成功和数据的一致性。