The ABA Problem

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是很难排查的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值