无锁队列 原理学习与实现

原始队列存在的问题

原始队列的入队与出队伪代码:

struct Node{
    void *data;
    Node *next;
};

void Enqueue(Node *node){
    m_Tail->next = node;
    m_Tail = node;
}

Node* Dequeue(){
    Node * res = m_Head;
    m_Head = m_Head->next;
    return res;
}

入队与出队皆有两步操作,如果多个线程同时进行读写,便可能会出现:完成了第一步操作后,其他线程修改了Head或Tail指针,导致结果无法预料。

例如下列情况,两个线程同时push:节点C和D都受到了影响

//        线程A                    线程B                        实际情况
                                                       A->B        head = A  tail = B
 入队C tail->next = C                                  A->B->C     head = A  tail = B
                            入队D tail->next = D       A->B->D     head = A  tail = B
                                   tail = D            A->B->D     head = A  tail = D
       tail = C                                        A->B->D     head = A  tail = C

无锁队列

无锁队列是在不使用传统锁(如互斥锁或读写锁)的情况下,实现线程安全的队列操作。
无锁队列主要依赖于现代处理器提供的原子操作(Atomic Operations)和内存模型来保证操作的线程安全性。

1. 原子操作

  • 原子性:原子操作是指不可中断的操作,即在执行过程中不会被其他线程打断,从而保证了操作的完整性。常见的原子操作包括原子的读、写、交换(Compare-and-Swap, CAS)、加载(Load)和存储(Store)等。
  • Compare-and-Swap (CAS)这是实现无锁数据结构的核心操作。CAS操作包含三个参数:内存位置、期望值和新值。仅当内存位置的值与期望值相匹配时,才会将新值写入内存位置,并返回true;否则,不做任何修改并返回false。

2. 冲突解决

  • 由于多个线程可能同时对队列做出修改,冲突在所难免。无锁队列通过循环重试机制来解决冲突。当一个线程发现其操作未成功时,它会重新读取状态并再次尝试操作,直至成功。

3. 内存模型

  • 为了确保不同线程之间对共享数据的访问是一致的,无锁队列的设计必须严格遵循处理器的内存模型。这涉及到内存屏障(Memory Barriers)的使用,以确保原子操作的可见性和顺序性。

4. 队列操作步骤(务必配合下面的代码一起看)

  • 插入(Push)操作
  • 创建新节点,并设置其数据字段。
  • 读取当前尾指针。
  • 尝试将新节点链接到当前尾节点的后面,使用CAS操作更新尾节点的next指针。
  • 如果CAS失败(说明尾节点已经改变),则重复前两步。
  • 如果CAS成功,尝试将尾指针更新为新节点(再次使用CAS),以确保后续入队操作能够正确地添加到队列末尾。
  • 删除(Pop)操作
  • 读取当前头指针和头节点的下一个节点。
  • 如果头节点的下一个节点为空,这是队列为空,无法出队。
  • 否则,尝试将头指针更新为头节点的下一个节点(使用CAS)。
  • 如果CAS成功,返回原头节点的数据字段,并删除原头节点。

5. 示例

#include <atomic>  
#include <memory>  
  
template <typename T>  
class LockFreeLinkedListQueue {  
private:  
    struct Node {  
        std::shared_ptr<T> data;  
        std::atomic<Node*> next;  
  
        Node(T new_data) : data(std::make_shared<T>(new_data)), next(nullptr) {}  
    };  
  
    std::atomic<Node*> head;  
    std::atomic<Node*> tail;  
  
public:  
    LockFreeLinkedListQueue() : head(new Node(T())), tail(head.load()) {}  
  
    ~LockFreeLinkedListQueue() {  
        Node* curr = head.load();  
        while (curr) {  
            Node* toDelete = curr;  
            curr = curr->next.load();  
            delete toDelete;  
        }  
    }  
  
    bool enqueue(T new_value) {  
        Node* new_node = new Node(new_value);  
        while (true) {  
            Node* old_tail = tail.load();  
            Node* next = old_tail->next.load();  
            if (old_tail == tail.load()) {//确保在上面两个读操作期间,队列没有被其他线程改变
                if (next == nullptr) {
                	//链接新节点
                    if (old_tail->next.compare_exchange_strong(next, new_node)) {
                    	// 更新尾指针
                        tail.compare_exchange_strong(old_tail, new_node);  
                        return true;  
                    }  
                } else {//其他线程已经插入了一个新节点
                	// 将tail更新
                    tail.compare_exchange_strong(old_tail, next);  
                }  
            }  
        }  
        return false;  
    }  
  
    bool dequeue(T& value) {
        while (true) {
            Node* old_head = head.load();
            Node* next = old_head->next.load();
            if (old_head == head.load()) {
            	// 注意这里是判断next
                if (next == nullptr) {
                    return false; // Queue is empty
                }
                if (head.compare_exchange_strong(old_head, next)) {
                    value = *next->data;
                    delete old_head;
                    return true;
                }
            }
        }
    }
};

出队的例子

T2线程在compare_exchange_weak后,发现不一致,会重新进入循环。

        T1线程                       T2线程                               实际情况
        -----                       -----                   A-B-C
        
oldHead = head.load();										A-B-C   T1-oldHead:A
                            oldHead = head.load();          A-B-C   T1-oldHead:A    T2-oldHead:A
                            next = oldHead->next.load();    A-B-C   T1-oldHead:A    T2-oldHead:A T2-next:B
next = oldHead->next.load();                                A-B-C   T1-oldHead:A T1-next:B       T2-oldHead:A T2-next:B
compare_exchange_weak                                       B-C     head=B       				 T2-oldHead:A T2-next:B
                            compare_exchange_weak           B-C     (head 与T2-oldHead不一致)
                                ..重新执行循环..

我们不能太快删除节点使用的内存,因为出队成功的节点可能还在被其他线程访问。比较好的方法是使用对象池来缓存节点,不够用的时候就申请新节点,每次出队使用完成后将旧节点放回池中等待下一次使用。

ABA问题

link

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值