CAS与无锁队列

CAS

        CAS(Compare-And-Swap)是一种原子操作,它在多线程编程中用于实现无锁数据结构和同步机制。CAS操作提供了一种机制,允许线程在没有使用传统锁的情况下,安全地更新共享数据。说白了CAS就是对比与交换, 它将一个内存位置上的内容与一个给定的值进行比较,只有它们相同时才将该内存位置的内容修改为新的给定值。 

原理

CAS操作的组成,CAS操作通常涉及三个主要参数:

  • 内存位置(V):这是要更新的变量的内存地址。
  • 预期原值(A):这是线程期望在执行CAS操作时内存位置V的值。
  • 新值(B):如果内存位置V的值与预期原值A相等,那么CAS操作将V更新为这个新值B。

CAS操作的步骤:

  1. 比较:线程检查内存位置V的当前值是否与预期原值A相等。
  2. 交换:如果当前值与预期原值相等,线程将内存位置V的值更新为新值B。
  3. 返回:CAS操作返回内存位置V的原始值(在更新之前)。

CAS操作的特点:

  • 原子性:CAS操作是原子的,这意味着它作为一个不可分割的操作执行,没有其他线程可以在CAS操作执行的过程中插入其他操作。
  • 非阻塞:与使用锁相比,CAS操作不会使线程进入阻塞状态。如果CAS操作失败(即当前值不等于预期原值),线程可以选择重试。
  • 自旋:在CAS操作失败时,线程可能会进入自旋等待状态,即不断重试CAS操作,直到成功为止。这可能导致CPU资源的浪费,特别是在高竞争环境下。

CAS操作的问题:

  • ABA问题:如果一个值原来是A,变成了B,又变回A,那么CAS操作会认为值没有变化,因为它比较的是A。这可能会导致一些逻辑错误。为了解决这个问题,通常使用版本号或时间戳来区分不同的值。
  • 伪共享:在多核处理器上,如果多个线程频繁访问同一缓存行,可能会导致性能下降。这是因为缓存行的一致性需要在多个核心之间同步,即使它们访问的是不同的数据。
  • 循环时间长开销:在高竞争环境下,CAS操作可能会失败很多次,导致线程在自旋等待中消耗大量CPU资源。

API

        在c++11中提供了原子操作库,也就是<atomic>。提供了两个API:

  • std::atomic<T>::compare_exchange_weak(T &expected, T desired)
  • std::atomic<T>::compare_exchange_strong(T &expected, T desired)

        函数内部实现的就是比较与交换,expected是期待值,也就是作比较的值,desired值是目标值,也就是修改值。weak与strong两个版本实现原理是一样的,只是如字面意思,weak较弱。

        weak不保证atomic<T>值与expected值相等的时候也会做交换,可能会在没有实际发生内存值变化的情况下返回false。这意味着即使内存位置的值在比较之后和预期值相等,也可能因为其他线程的干预而失败。

        strong在CAS操作失败时会提供更强的保证。它会在比较失败后立即重试,直到成功或直到内存位置的值不再等于预期值。它可能会在高竞争环境下导致更多的CPU资源消耗

#include<iostream>
#include<atomic>

std::atomic<int> cnt(3000);
int cmp=3000;
int des=1000;
bool exchanged=false;

int main(){

    exchanged=cnt.compare_exchange_strong(cmp,des);
    std::cout<<exchanged<<std::endl;

    exchanged=cnt.compare_exchange_weak(cmp,des);
    std::cout<<exchanged<<std::endl;

    return 0;
}

        我们尝试对原子变量cnt进行CAS操作,得出结果:第一次成功,第二次失败。

无锁队列

        我们常见的,普通的生产者-消费者模型队列,在多线程环境下加锁与解锁是需要时间的,线程的上下文切换代价也比较大,此外还有cache损坏等等,种种原因会导致性能下降,这时候无锁队列会是更明智的选择。无锁队列更适合非耗时任务,因为频繁地锁操作在整个系统占比一旦过大就会降低整体性能。

原理与实现

        我们知道,原子操作和CAS是不需要锁的,那么可以使用原子操作+CAS+链表来实现无锁队列。无锁队列就拨云见日、茅塞顿开了。此外我们也可以选择使用数组,这里我们使用链表。

        首先来看原子操作,如果我们将一个变量定义为原子变量,那么我们对这个变量的所有操作都将会是原子操作,如果不明白可以看我的另一篇博客:原子操作与锁。我们可以将链表节点设置为原子变量:

public:
    struct Node {
        T data;//数据
        std::atomic<Node*> next;
        Node(T const& data) : data(data), next(nullptr) {}
    };

private:
    std::atomic<Node*> head;//头节点
    std::atomic<Node*> tail;//尾节点

        再来看CAS操作,我们可以利用CAS来实现入队与出队操作。入队时,我们先用load读取当前尾部的节点值,再用获取的尾部值与当前尾部值比较,如果相同,就说明这个值没有被其他线程修改(我们使用weak,这样就不会替换相同值),如果不同,就说明在这期间其他线程已经修改了这个值,我们就要循环到下一次重新获取尾部值,这就是CAS的使用场景;出队时,我们同样使用load获取头部值,只不过需要判断,如果头尾指针相同说明队列为空。这时候出队失败。

 void enqueue(T const& data) {
        Node* new_node = new Node(data);
        Node* old_tail = tail.load();
        while (true) {
            old_tail->next = new_node;
            if (tail.compare_exchange_weak(old_tail, new_node)) {
                return;
            }
        }
    }

    bool dequeue(T& result) {
        Node* old_head = head.load(std::memory_order_relaxed);//无顺序
        while (true) {
            if (old_head == tail.load(std::memory_order_relaxed)) {
                return false; // 队列为空
            }
            Node* next = old_head->next.load(std::memory_order_relaxed);
            if (head.compare_exchange_weak(old_head, next)) {
                result = std::move(next->data);
                delete old_head;
                return true;
            }
        }
    }

        咱们将这两个操作封装成一个简单的无锁队列,测试一下。

#include<iostream>
#include<atomic>
#include<thread>
#include<vector>
#include<cassert>

template <typename T>
class unlockQueue{
public:
    struct Node{
        T data;
        std::atomic<Node*>next;
        Node(T const& data):data(data),next(nullptr){}
    };

private:
    std::atomic<Node*>head;
    std::atomic<Node*>tail;

public:
    unlockQueue():head(new Node(T())),tail(head.load(std::memory_order_relaxed)){}
    ~unlockQueue(){
        while(Node*const old_head=head.load(std::memory_order_relaxed)){
            head.store(old_head->next,std::memory_order_relaxed);//存储下一个节点值
            delete old_head;//删除旧节点值
        }
    }
    unlockQueue(const unlockQueue&other)=delete;//禁用拷贝构造
    unlockQueue&operator=(const unlockQueue&other)=delete;//禁用赋值操作
    void enqueue(T const& data) {
        Node* new_node = new Node(data);
        Node* old_tail = tail.load();
        while (true) {
            old_tail->next = new_node;
            if (tail.compare_exchange_weak(old_tail, new_node)) {
                return;
            }
        }
    }
    bool dequeue(T& result) {
        Node* old_head = head.load(std::memory_order_relaxed);
        while (true) {
            if (old_head == tail.load(std::memory_order_relaxed)) {
                return false; // 队列为空
            }
            Node* next = old_head->next.load(std::memory_order_relaxed);
            if (head.compare_exchange_weak(old_head, next)) {
                result = std::move(next->data);
                delete old_head;
                return true;
            }
        }
    }
};

void producer(unlockQueue<int>& queue, int start, int count) {//生产,往队列里加任务
    for (int i = 0; i < count; ++i) {
        queue.enqueue(start + i);
    }
}

void consumer(unlockQueue<int>& queue) {//消费,从队列取任务
    int value;
    while (true) {
        if (!queue.dequeue(value)) {
            break; 
        }
        std::cout << "Consumed: " << value << std::endl;
    }
}

int main() {
    const int producer_count = 2;
    const int consumer_count = 2;
    const int items_per_producer = 50;
    unlockQueue<int> queue;

    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    // Start producers
    for (int i = 0; i < producer_count; ++i) {
        //每个生产者线程开始入队整数的起始值。由于有多个生产者线程,每个线程都会从不同的起始值开始入队,以避免它们尝试入队相同的值。
        producers.emplace_back(producer, std::ref(queue), i * items_per_producer, items_per_producer);
    }

    // Start consumers
    for (int i = 0; i < consumer_count; ++i) {
        consumers.emplace_back(consumer, std::ref(queue));
    }

    // Join producers
    for (auto& producer : producers) {
        producer.join();
    }

    // Join consumers
    for (auto& consumer : consumers) {
        consumer.join();
    }

    std::cout << "All items have been consumed." << std::endl;

    return 0;
}

        在实际项目中还需按情况修改与调整。

  • 15
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值