Lock Free 无锁队列的实现

无锁队列的实现

 

无锁队列的实现原理一般是利用 Retry-loop 和 CAS 等原子操作。

现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令。

例如 CAS(Compare And Swap)的实现原理:

bool compare_and_swap (int *addr, int oldval, int newval)
{
  if ( *addr != oldval ) {
      return false;
  }
  *addr = newval;
  return true;
}

与CAS相似的还有下面的原子操作:

1、Fetch And Add,一般用来对变量做+1的原子操作;
2、Test And Set,写值到某个内存位置并传回其旧值;
3、Test And Test-and-set;
 
GCC 的CAS实现版本如下:
bool __sync_bool_compare_and_swap(type *ptr, type oldval, type newval, ...)

可参考http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html

 
C++11 的CAS实现版本如下:
template<class T> 
bool atomic_compare_exchange_weak(atomic<T>* obj, T* expected, T desired);

 

看一个无锁队列的实现例子:

class QueueWithoutLock {
public:
    QueueWithoutLock() {
        dummy = new ListNode(-1);
        head = tail = dummy;
    }

    ~QueueWithoutLock() {}

    bool Push(uint64_t val) {
        auto* node = new ListNode(val);
        ListNode* oldTail = nullptr;
        do {
            oldTail = tail;  // 如果其它线程 push 成功,这里 tail 可能发生变化,所以重新取值一下
        } while (!__sync_bool_compare_and_swap(&(oldTail->next), nullptr, node));

        // 更新 tail 到新节点
        // 这里没有判断成功,是因为走到这里,表示上面成功 push 了,在更新 tail 之前其 next 不再是 null,其它线程的 push 操作都会失败
        __sync_bool_compare_and_swap(&tail, oldTail, node);
        return true;
    }

    ListNode* Pop() {
        ListNode *p;
        do {
            p = head;
            if (p->next == NULL) {
                // queue is empty
                return nullptr;
            }
        } while (!__sync_bool_compare_and_swap(&head, p, p->next));     // 如果 head != p(说明其它线程修改了 head),循环继续
        return p->next;
    }

private:
    ListNode* dummy;
    ListNode* head;
    ListNode* tail;
};

 

这里有一个潜在的问题——如果某个线程在用 CAS 更新 tail 指针之前,线程停掉或是挂掉了,那么其它线程就进入死循环了。下面是改良版的Push()

bool Push(uint64_t val) {
    auto* node = new ListNode(val);
    ListNode* oldTail = tail;       // 取链表尾指针的快照
    ListNode* p = tail;
    do {
        while (p->next) p = p->next;     // 每次循环里面重新 fetch 尾指针,不依赖 tail 成员
    } while (!__sync_bool_compare_and_swap(&(p->next), nullptr, node));

    // 重置 tail 指针
    __sync_bool_compare_and_swap(&tail, oldTail, node);
    return true;
}

 

 

CAS 导致的 ABA 问题

 

有2个线程 a、b,

  1. 线程 a 从共享的地址X中读取到了对象A。
  2. 在线程 a 准备对地址X进行更新之前,线程b将地址X中的值修改为了B。
  3. 接着线程 b 将地址X中的值又修改回了A。
  4. 最新线程 a 对地址X执行 CAS,发现X中存储的还是对象A,对象匹配,CAS成功。

在有 GC环境的编程语言比如说java中,2,3的情况是不可能出现的,因为在java中,只要两个对象的地址一致,就表示这两个对象是相等的。

但在 C++ 中,可以自己控制对象的生命周期,如果我们从一个 list 中删除掉了一个对象,然后又重新分配了一个对象,并将其add back到 list 中去,那么根据 MRU memory allocation算法,这个新的对象很有可能和之前删除对象的内存地址是一样的。这样就会导致ABA的问题。

解决办法:

1、使用 风险指针(hazard pointer),线程a 读出对象A的同时,将对象A的指针放入到自己的 hp 链表中,这样可以保证这个对象不会被删除;

2、使用 read-copy update (RCU) ,在每次更新的之前,都做一份拷贝,每次更新的是拷贝出来的新结构。

 

 

 

 

posted @ 2024-11-13 16:49  如果的事  阅读(7)  评论(0编辑  收藏  举报