ABA problem
在多线程环境下,在同步的过程中可能会发生ABA问题。如果一个线程对同一片内存区域进行两次读取,发现两次读取的内容相同,那么它会认为在这个两次读取过程中系统状态没有发生改变,可以对内存区域进行修改,从而不会造成一致性问题(这就是CAS的基本原理,使用CAS来做线程同步的话,一般先会读取变量的值,然后对变量的值进行修改,最后通过CAS原子指令比较之前读取的值与内存中的值是否相同,如果相同就将修改后的值,写回到内存中)。然而,在执行CAS执行之前,线程的执行流可能会被打断,也就是说在读取变量值之后,到使用CAS指令修改变量值这个过程中,可能会有其他线程修改了变量的值,然后再将变量的值修改为修改之前的值,当前一个线程恢复执行,并执行CAS指令,它发现内存中的变量值和之前读到的值一样,所以就误认为系统状态未发生改变,从而执行对变量的修改操作,因为另一个线程对变量的修改,可能改变了系统的状态,所以当前一个线程通过CAS指令执行修改操作时,可能会引发系统异常。
当多个线程交替访问临界资源的时候可能会发生ABA问题,下面两个线程的执行过程复现了ABA问题:
- P1读取到共享变量的值为A
- P1的执行权限被抢占,然后P2开始占用CPU
- 在P1恢复执行之前,P2将共享变量的值先从A修改为B,然后再将B修改为之前的A
- P1重新获得时间片,然后发现共享变量的值未发生改变,因此继续执行后面的操作
虽然,P1重新获得了时间片,能够继续执行后面的操作,但是因为P2在P1被抢占之后,因为P2执行的某些操作可能会对系统的状态有影响,因此如果P1继续执行后面的代码,可能导致系统状态的不一致,或者引发异常。
ABA问题发生的一个常见场景就是实现无锁数据结构,假设在一个无锁的List中,一个元素被删除,紧接着一个新的元素被创建并添加进来,因为系统的优化,新的元素在内存中的位置可能和被删除的元素在内存中的位置相同,也就是说,新元素和被删除的元素的指针是指向同一个内存地址,这个时候就发生了ABA问题。
Examples
John驾车停在了一个红灯的路口,然后车内的孩子开始打闹,John开始安抚孩子们,当孩子们安静下来不再打闹的时候,他再一次检查了指示灯,发现还是红灯。然后,其实当他在安抚孩子的时候,指示灯已经变成绿色,并且又变回了红色。但是John并没有观察到这个变化。
在这个例子中,ABA问题中状态’A’表示指示灯为红色,’B’表示指示灯为绿色。刚开始的时候,指示灯为红色(状态’A’),如果John一直在观察指示灯的状态的话,他肯定不会错过这个变化,但是他两次观察并不是连续的,因此在这两次观察的过程中,无法判断指示灯的颜色是否发生过变化。
下面是一另外一个ABA例子,无锁栈(lock-free stack):
/* Naive lock-free stack which suffers from ABA problem.*/
class Stack {
std::atomic<Obj*> top_ptr;
//
// 弹出并返回栈顶元素
//
Obj* Pop() {
while(1) {
Obj* ret_ptr = top_ptr;
if (!ret_ptr) return nullptr;
// 简单起见,假定我们能够保证解引用是安全的(也就是说没有其他线程在执行同样的操作,即出栈)
Obj* next_ptr = ret_ptr->next;
// 如果栈顶还是ret那么堆栈及堆栈元素没有被修改
// 我们知道因为ABA问题,上面的假定并不总是一直成立
// 弹出栈顶元素
if (top_ptr.compare_exchange_weak(ret_ptr, next_ptr)) {
return ret_ptr;
}
// The stack has changed, start over.
}
}
//
// 将指定的元素压入栈顶
//
void Push(Obj* obj_ptr) {
while(1) {
Obj* next_ptr = top_ptr;
obj_ptr->next = next_ptr;
// 如果栈顶还是next那么堆栈及堆栈中的元素未被修改过.
// 因为ABA问题存在,上面的假定并不一定成立
// 压入栈顶
if (top_ptr.compare_exchange_weak(next_ptr, obj_ptr)) {
return;
}
// The stack has changed, start over.
}
}
};
这段代码避免了并发访问中的问题,但是却引来了ABA问题,考虑下面的执行序列
初始堆栈结构如下:
top->A->B->C
Thread1开始执行:
ret = A;
next = B;
Thread1在执行compare_exchange_weak之前被中断
{ // Thread 2 runs pop:
ret = A;
next = B;
compare_exchange_weak(A, B) // Success, top = B
return A;
} // Now the stack is top → B → C
{ // Thread 2 runs pop again:
ret = B;
next = C;
compare_exchange_weak(B, C) // Success, top = C
return B;
} // Now the stack is top → C
delete B;
{ // Thread 2 now pushes A back onto the stack:
A->next = C;
compare_exchange_weak(C, A) // Success, top = A
}
现在堆栈结构:
top->A->C
所以当Thread1恢复执行:
compare_exchange_weak(A, B)
因为现在栈顶元素确实是A,所以Thread1将会用B去替换栈顶元素,但是B已经被Thread2给删除了,所以Thread1将会访问一个被释放了的内存地址,这种行为在C++中是未定义的,什么情况都可能发生。所以ABA问题造成的Bug是很难排查的