当前位置: 首页 > news >正文

C++ 无锁队列:原理与实现

引言

在多线程编程中,队列是一种常用的数据结构。传统的队列在多线程环境下访问时,通常需要使用锁机制来保证数据的一致性和线程安全。然而,锁的使用会带来性能开销,尤其是在高并发场景下,频繁的加锁和解锁操作可能成为性能瓶颈。无锁队列作为一种高效的替代方案,通过使用原子操作和巧妙的内存管理,避免了锁的使用,从而提升了多线程环境下的性能。本文将深入探讨 C++ 中无锁队列的实现原理,并给出详细的代码示例。

无锁队列的原理

无锁队列基于 CAS(Compare - And - Swap)操作来实现。CAS 是一种原子操作,它将内存位置的内容与给定值进行比较,如果相等,则将该内存位置的内容修改为新的值。在无锁队列中,入队和出队操作通过 CAS 操作来更新队列的头部和尾部指针,从而避免了锁的使用。

假设有一个简单的节点结构用于构成队列:

 

template<typename T>

struct Node {

T data;

std::atomic<Node*> next;

Node(const T& value) : data(value), next(nullptr) {}

};

这里的next指针被声明为std::atomic<Node*>类型,以确保对其进行的操作是原子的,避免多线程环境下的竞态条件。

入队操作

入队操作的基本步骤如下:

  1. 创建一个新节点,将数据放入新节点。
  1. 找到队列的尾部节点。
  1. 使用 CAS 操作将新节点链接到尾部节点的next指针上。
  1. 如果 CAS 操作失败,说明在找到尾部节点后,队列发生了变化(其他线程进行了入队或出队操作),需要重新执行步骤 2 和 3。
  1. 成功链接新节点后,使用 CAS 操作更新队列的尾部指针指向新节点。

出队操作

出队操作的基本步骤如下:

  1. 检查队列是否为空。
  1. 找到队列的头部节点。
  1. 获取头部节点的下一个节点。
  1. 使用 CAS 操作将队列的头部指针更新为下一个节点。
  1. 如果 CAS 操作失败,说明在获取头部节点后,队列发生了变化,需要重新执行步骤 2 到 4。
  1. 释放旧的头部节点。

无锁队列的实现代码

 

template<typename T>

class LockFreeQueue {

private:

std::atomic<Node<T>*> head;

std::atomic<Node<T>*> tail;

public:

LockFreeQueue() {

Node<T>* sentinel = new Node<T>(T());

head.store(sentinel);

tail.store(sentinel);

}

~LockFreeQueue() {

while (Node<T>* node = head.load()) {

head.store(node->next.load());

delete node;

}

}

void enqueue(const T& value) {

Node<T>* newNode = new Node<T>(value);

while (true) {

Node<T>* tailNode = tail.load();

Node<T>* nextNode = tailNode->next.load();

if (tailNode == tail.load()) {

if (!nextNode) {

if (tailNode->next.compare_exchange_weak(nextNode, newNode)) {

tail.compare_exchange_weak(tailNode, newNode);

return;

}

}

else {

tail.compare_exchange_weak(tailNode, nextNode);

}

}

}

}

bool dequeue(T& result) {

while (true) {

Node<T>* headNode = head.load();

Node<T>* tailNode = tail.load();

Node<T>* nextNode = headNode->next.load();

if (headNode == head.load()) {

if (headNode == tailNode) {

if (!nextNode) {

return false;

}

tail.compare_exchange_weak(tailNode, nextNode);

}

else {

result = nextNode->data;

if (head.compare_exchange_weak(headNode, nextNode)) {

delete headNode;

return true;

}

}

}

}

}

};

代码解析

  1. 构造函数:在构造函数中,创建一个哨兵节点(sentinel node),并将head和tail指针都指向该节点。哨兵节点用于简化边界条件的处理,避免在队列为空时进行额外的判断。
  1. 析构函数:析构函数遍历队列,释放所有节点的内存。它通过不断地加载head指针,并将其更新为下一个节点,然后删除当前节点,直到head指针为空。
  1. 入队函数enqueue
    • 首先创建一个新节点。
    • 进入一个无限循环,在循环中不断尝试找到队列的尾部节点。
    • 检查当前找到的尾部节点的next指针是否为空。如果为空,尝试使用compare_exchange_weak(一种 CAS 操作)将新节点链接到尾部节点的next指针上。如果链接成功,再尝试更新tail指针指向新节点。
    • 如果当前找到的尾部节点的next指针不为空,说明有其他线程在当前线程找到尾部节点后进行了入队操作,此时需要更新tail指针,使其指向next指针所指向的节点,然后重新尝试入队。
  1. 出队函数dequeue
    • 进入一个无限循环,在循环中不断尝试找到队列的头部节点。
    • 检查队列是否为空(即head和tail指针是否指向同一个节点且该节点的next指针为空)。如果为空,返回false表示出队失败。
    • 如果队列不为空,获取头部节点的下一个节点的数据。
    • 使用compare_exchange_weak尝试更新head指针,使其指向头部节点的下一个节点。如果更新成功,说明出队操作成功,删除旧的头部节点并返回true。
    • 如果更新失败,说明有其他线程在当前线程获取头部节点后进行了入队或出队操作,此时需要重新尝试出队。

无锁队列使用示例

下面是一个简单的多线程环境中使用无锁队列的示例代码,展示了如何创建无锁队列对象,以及多个线程同时进行入队和出队操作:

 

#include <iostream>

#include <thread>

#include <vector>

#include "LockFreeQueue.h" // 假设无锁队列实现代码在该头文件中

void producer(LockFreeQueue<int>& queue, int value) {

queue.enqueue(value);

std::cout << "Produced: " << value << std::endl;

}

void consumer(LockFreeQueue<int>& queue) {

int result;

if (queue.dequeue(result)) {

std::cout << "Consumed: " << result << std::endl;

}

else {

std::cout << "Queue is empty" << std::endl;

}

}

int main() {

LockFreeQueue<int> queue;

std::vector<std::thread> threads;

// 创建多个生产者线程

for (int i = 0; i < 3; ++i) {

threads.emplace_back(producer, std::ref(queue), i * 10);

}

// 创建多个消费者线程

for (int i = 0; i < 3; ++i) {

threads.emplace_back(consumer, std::ref(queue));

}

// 等待所有线程完成

for (auto& thread : threads) {

thread.join();

}

return 0;

}

在上述示例中,定义了producer和consumer函数,分别用于向无锁队列中入队和出队数据。在main函数中,创建了一个LockFreeQueue<int>对象,并启动了多个生产者线程和消费者线程。生产者线程向队列中插入不同的值,消费者线程从队列中取出数据并打印。通过这种方式,可以直观地看到无锁队列在多线程环境下的工作情况。

性能优势与适用场景

无锁队列在高并发场景下具有显著的性能优势,因为它避免了锁带来的线程阻塞和上下文切换开销。在多线程频繁访问队列的情况下,无锁队列能够提供更高的吞吐量和更低的延迟。然而,无锁队列的实现相对复杂,代码可读性和可维护性较差。因此,在低并发场景下,或者对代码复杂度有严格要求的场景中,传统的带锁队列可能是更合适的选择。

总结

无锁队列是一种在多线程编程中非常有用的数据结构,通过巧妙地使用原子操作和内存管理,避免了锁的使用,从而提升了性能。本文详细介绍了无锁队列的原理,并给出了完整的 C++ 实现代码及使用示例。希望通过本文的介绍,读者能够深入理解无锁队列的工作机制,并在实际项目中灵活运用这一技术。

http://www.lryc.cn/news/540518.html

相关文章:

  • web的分离不分离:前后端分离与不分离全面分析
  • MobileSal:极其高效的RGB-D显著性物体检测模型
  • 【个人总结】1. 开发基础 工作三年的嵌入式常见知识点梳理及开发技术要点(欢迎指正、补充)
  • 硬核技术组合!用 DeepSeek R1、Ollama、Docker、RAGFlow 打造专属本地知识库
  • MySQL官网驱动下载(jar包驱动和ODBC驱动)【详细教程】
  • idea 2019.3常用插件
  • 对CSS了解哪些?
  • TikTok账户安全指南:如何取消两步验证?
  • 从零到一:构建现代 React 应用的完整指南
  • 【Python爬虫(26)】Python爬虫进阶:数据清洗与预处理的魔法秘籍
  • 机器学习数学基础:28.卡方检验
  • 【工具插件类教学】实现运行时2D物体交互的利器Runtime2DTransformInteractor
  • 回调处理器
  • Redis-03高级篇中-多级缓存:
  • Spring Boot ShardingJDBC分库分表(草稿)
  • Jenkins 环境搭建---基于 Docker
  • 如何在自定义组件中使用v-model实现双向绑定
  • Ubuntu 下 nginx-1.24.0 源码分析 - ngx_pool_t 类型
  • 如何在 ubuntu 上使用 Clash 与 docker 开启代理拉起
  • linux tcpdump抓包
  • PLSQL连接Oracle 19c报错ORA-28040
  • 汽车免拆诊断案例 | 2010 款路虎揽胜车空调偶尔出风异常
  • 音视频入门基础:RTP专题(9)——FFmpeg接收RTP流的原理和内部实现
  • Nginx 安装及配置教程(Windows)【安装】
  • 《跟李沐学 AI》AlexNet论文逐段精读学习心得 | PyTorch 深度学习实战
  • 嵌入式0xDEADBEEF
  • B+树作为数据库索引结构的优势对比
  • 自适应SQL计划管理(Adaptive SQL Plan Management)在Oracle 12c中的应用
  • 什么是DeFi (去中心化金融)
  • 计算机毕业设计Python农产品推荐系统 农产品爬虫 农产品可视化 农产品大数据(源码+LW文档+PPT+讲解)