atomic原子操作实现无锁队列

一、atomic概念

原子操作是指不可中断的操作,即一个操作的执行过程中,不会被其他线程打断,要么全部执行完毕,要么根本不执行。

举个简单例子:多线程环境下,对一个变量i执行i++操作,看似是一个单一操作,实则包含三个步骤:读取i的值、将i的值加1、将结果写回i。如果没有原子性保证,多个线程同时执行时,会出现线程安全问题(比如两个线程同时读取i=1,都加1后写回,最终i=2,而非预期的3)。

atomic的核心作用

  • 解决多线程环境下的数据竞争问题,避免线程安全隐患;

  • 替代 synchronized 锁,实现更高效的并发控制(atomic基于CAS机制,无锁操作,性能优于锁);

  • 保证操作的原子性、可见性和有序性(部分场景下)。

二、atomic实现无锁队列

完整代码如下:

cpp 复制代码
#include<iostream>
#include<atomic>
using namespace std;

// 队列节点结构
template<typename T>
struct Node {
	T data;       // 节点存储的数据
	Node* next;   // 指向后一个节点的指针
	// 节点构造函数,初始化数据和next指针
	Node(T val) : data(val), next(nullptr) {}
};

// 无锁队列类(模板实现,支持任意数据类型)
template<typename T>
class LockFreeQueue {
private:
	// 原子指针:head(队列头)、tail(队列尾)
	atomic<Node<T>*> head;
	atomic<Node<T>*> tail;
public:
	// 构造函数:初始化队列(带头节点,避免空队列判断复杂)
	LockFreeQueue() {
		// 创建哑节点(dummy node),作为队列的哨兵节点
		Node<T>* dummy = new Node<T>(T{});
		// 原子存储:将head和tail都指向哑节点
		head.store(dummy);
		tail.store(dummy);
	}

	// 入队操作:将数据val加入队列尾部
	void enqueue(T val) {
		// 1. 创建新节点,存储待入队数据
		Node<T>* newNode = new Node<T>(val);
		// 2. 原子交换:将tail指向新节点,并返回原来的tail(oldTail)
		// memory_order_acq_rel:保证原子操作的内存可见性和有序性
		Node<T>* oldTail = tail.exchange(newNode, memory_order_acq_rel);
		// 3. 将原来的尾节点的next指向新节点,完成链表链接
		oldTail->next = newNode;
	}

	// 出队操作:将队列头部数据取出,存入val,返回是否出队成功
	bool dequeue(T& val) {
		// 1. 原子加载head指针(获取当前队列头)
		Node<T>* oldHead = head.load(memory_order_acquire);
		// 2. 获取head的下一个节点(真正存储数据的节点,哑节点不存数据)
		Node<T>* next_node = oldHead->next;

		// 3. 判断队列是否为空(next_node为nullptr,说明只有哑节点)
		if (next_node == nullptr) {
			return false; // 队列为空,出队失败
		}

		// 4. 取出next_node的数据(真正要出队的数据)
		val = next_node->data;
		// 5. 原子存储:将head指向next_node,完成头节点更新
		head.store(next_node, memory_order_release);
		// 6. 释放原来的头节点(哑节点),避免内存泄漏
		delete oldHead;
		return true; // 出队成功
	}

	// 析构函数:释放队列所有节点,避免内存泄漏
	~LockFreeQueue() {
		Node<T>* cur = head.load();
		while (cur != nullptr) {
			Node<T>* tmp = cur;
			cur = cur->next;
			delete tmp;
		}
	}
};

// 测试代码
int main()
{
	// 实例化一个存储int类型的无锁队列
	LockFreeQueue<int> q;
	// 入队两个元素
	q.enqueue(10);
	q.enqueue(20);

	int val;
	// 出队并打印结果
	if (q.dequeue(val))
	{
		cout << "Dequeued: " << val << endl;
	}
	if (q.dequeue(val))
	{
		cout << "Dequeued: " << val << endl;
	}

	return 0;
}
复制代码
运行输出:
bash 复制代码
Dequeued: 10 Dequeued: 20

代码拆解:

1 节点结构(Node)

队列采用单向链表实现,每个Node节点包含两个核心成员:

  • data:存储具体的数据,支持任意类型(通过模板T实现);

  • next:指向后一个节点的指针,用于维护链表的连续性。

构造函数Node(T val)用于初始化节点,将数据val存入data,并将next指针置为nullptr(初始状态下无后续节点)。

2 无锁队列类(LockFreeQueue)

该类是核心,通过两个原子指针(atomic<Node<T>*>)实现无锁的入队和出队操作,避免了传统锁机制的开销。

2.1 私有成员:原子指针head和tail

核心设计:用atomic包装Node指针,保证对指针的读写操作是原子性的------即多个线程同时读写head/tail时,不会出现"读取一半被打断""写入冲突"等问题。

为什么用原子指针:普通指针的读写的非原子的,比如线程A读取head指针,线程B同时修改head指针,可能导致线程A读取到一个"无效指针"(只读取了指针的一半字节),引发程序崩溃。而atomic<Node*>会通过CPU原子指令,保证指针的读写操作完整、不可中断。

2.2 构造函数:初始化哑节点

队列初始化时,创建一个哑节点(dummy node),并将head和tail都指向这个哑节点。

设计哑节点的核心目的是避免空队列的复杂判断------如果没有哑节点,队列空时head和tail都为nullptr,入队、出队时需要额外判断nullptr,容易引发并发问题;有了哑节点,队列始终至少有一个节点(哑节点),head和tail永远不会为nullptr,简化了逻辑。

2.3 入队操作(enqueue)

入队操作是无锁队列的核心,全程无锁,通过原子操作保证线程安全,步骤拆解:

  1. 创建新节点:Node<T>* newNode = new Node<T>(val);,将待入队的数据val存入新节点;

  2. 原子交换tail指针:tail.exchange(newNode, memory_order_acq_rel);

    1. exchange是atomic的核心成员函数:将tail的值(当前尾节点指针)替换为newNode,并返回替换前的tail值(oldTail);

    2. memory_order_acq_rel:内存序,保证该原子操作的"获取-释放"语义------确保所有线程能看到该操作之前的内存写入,同时该操作的写入能被后续线程看到,避免指令重排导致的并发问题。

  3. 链接新节点:oldTail->next = newNode;,将原来的尾节点(oldTail)的next指针指向新节点,完成链表的链接,此时新节点成为新的尾节点。

为什么入队不需要锁?

因为tail.exchange是原子操作,多个线程同时入队时,只有一个线程能成功将tail替换为自己的新节点,其他线程会拿到更新后的tail(前一个线程的新节点),并将自己的新节点链接到该tail之后,不会出现节点链接混乱。

2.4 出队操作(dequeue)

出队操作同样基于原子操作,步骤拆解:

  1. 原子加载head指针:Node<T>* oldHead = head.load(memory_order_acquire);,获取当前队列头(哑节点);

  2. 获取有效数据节点:Node<T>* next_node = oldHead->next;,next_node才是真正存储数据的节点(哑节点的next指向第一个有效节点);

  3. 判断队列是否为空:如果next_node为nullptr,说明队列中只有哑节点,无有效数据,返回false(出队失败);

  4. 取出数据:val = next_node->data;,将有效节点的数据存入val(通过引用传出);

  5. 原子更新head指针:head.store(next_node, memory_order_release);,将head指向next_node(新的哑节点);

  6. 释放旧哑节点:delete oldHead;,避免内存泄漏,返回true(出队成功)。

注意:memory_order_acquire(加载时)和memory_order_release(存储时)的配合,保证了出队操作的内存可见性------线程A出队后,线程B能立即看到head的更新,不会读取到旧的head指针。

3 测试代码(main函数)

测试逻辑简单易懂:

  1. 实例化一个存储int类型的无锁队列LockFreeQueue<int> q

  2. 调用enqueue入队两个整数(10、20);

  3. 调用dequeue出队,判断出队是否成功,若成功则打印出队数据;

  4. 运行后输出两个出队数据,验证队列功能正常。

相关推荐
Yungoal2 小时前
常见 时间复杂度计算
c++·算法
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 48. 旋转图像 | C++ 矩阵变换题解
c++·leetcode·矩阵
CHHC18802 小时前
NetCore树莓派桌面应用程序
linux·运维·服务器
Ricky_Theseus3 小时前
C++右值引用
java·开发语言·c++
帮我吧智能服务平台3 小时前
装备制造智能制造升级:远程运维与智能服务如何保障产线OEE
运维·服务器·制造
吴梓穆4 小时前
UE5 c++ 常用方法
java·c++·ue5
云栖梦泽4 小时前
Linux内核与驱动:9.Linux 驱动 API 封装
linux·c++
Morwit4 小时前
【力扣hot100】 1. 两数之和
数据结构·c++·算法·leetcode·职场和发展
SpiderPex4 小时前
第十七届蓝桥杯 C++ B组-题目 (最新出炉 )
c++·职场和发展·蓝桥杯