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

高性能C++实践:原子操作与无锁队列实现

一、 场景:高并发下的锁竞争之痛

在我参与的一个高频交易模拟系统中,我们遇到了一个典型的性能瓶颈。该系统中有一个核心组件——一个多生产者、多消费者模式的任务队列。各个网络I/O线程接收到数据后,会将计算任务压入这个队列,而一群工作线程则不断地从队列中取出任务进行处理。

最初,我们使用std::mutex来保护这个std::queue。在低并发下,它工作良好。但当我们将线程数量(生产者和消费者总和)提升到20以上,并模拟每秒数十万次的操作时,性能监控工具(如perf)显示,大量的CPU时间被消耗在了内核态的锁竞争上(大量的futex系统调用)。线程们大部分时间都在“等待”,而不是“工作”,CPU使用率居高不下但吞吐量却停滞不前。这正是粗粒度锁同步带来的典型问题。

二、 核心思路:拥抱原子操作与无锁编程

为了解决这个瓶颈,我们决定将传统的互斥锁队列替换为无锁队列(Lock-Free Queue)

  • 互斥锁(Mutex):是一种“悲观”的并发控制。它假设冲突很会发生,因此每次操作前都先加锁,强制线程排队串行访问共享资源。这导致了线程阻塞、上下文切换等高昂开销。
  • 无锁(Lock-Free):是一种“乐观”的并发控制。它利用CPU提供的原子操作(Atomic Operations)(如CAS - Compare-And-Swap)来直接操作共享数据。线程会尝试完成任务,如果失败(因为其他线程干扰),它会重试而不是阻塞。最坏情况下只会导致某个线程“忙等”,而绝不会导致整个系统阻塞,从而在高竞争下往往能提供更好的可伸缩性和稳定性。

我们的目标:实现一个多生产者多消费者(MPMC)的无锁队列,并通过基准测试量化其与互斥锁队列的性能差异。

三、 操作步骤与实现

无锁编程极其复杂且容易出错。在实际项目中,我们首选业界成熟的开源实现(如moodycamel::ConcurrentQueue)。但为了深入理解其原理,我们团队自己实现了一个基础版本。以下是简化后的核心实现步骤和代码。

1. 选择数据结构:单链表

我们选择基于单链表实现队列。每个节点包含数据和指向下一个节点的原子指针。队列本身包含两个原子指针:headtail

#include <atomic>
#include <memory>template<typename T>
class LockFreeQueue {
private:struct Node {std::shared_ptr<T> data; // 使用shared_ptr避免拷贝开销std::atomic<Node*> next;Node() : next(nullptr) {}};std::atomic<Node*> head;std::atomic<Node*> tail;public:LockFreeQueue() {// 初始化时,head和tail都指向一个哑元节点(dummy node)Node* dummy = new Node();head.store(dummy);tail.store(dummy);}~LockFreeQueue() {// 需要安全地删除所有节点,略}// ...
};

关键点:使用哑元节点可以简化pushpop操作边界条件的判断。

2. 实现Push操作
void Push(T new_value) {// 1. 准备新节点和数据std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));Node* new_node = new Node();// 2. 循环CAS直到成功将新节点链入尾部Node* old_tail = tail.load();Node* null_ptr = nullptr;while (true) {// 2.1 首先尝试将新节点链入当前tail的next指针if (old_tail->next.compare_exchange_weak(null_ptr, new_node)) {// CAS成功,说明新节点已链入break;} else {// CAS失败,说明其他线程已经修改了next,帮助推进tail然后重试// 这是无锁算法中常见的“帮助”机制Node* temp = null_ptr;tail.compare_exchange_weak(old_tail, old_tail->next);old_tail = tail.load();}}// 3. 尝试更新tail指针到新节点(即使失败也没关系,后续操作会帮助推进)tail.compare_exchange_strong(old_tail, new_node);
}

关键点

  • 使用compare_exchange_weak在循环中重试,它是无锁算法的基石。
  • push操作有两个关键步骤:链接新节点和推进tail。另一个线程的失败操作可能由本线程“帮助”完成,这是保证无锁进度(Lock-Free Progress)的关键。
3. 实现Pop操作
std::shared_ptr<T> Pop() {Node* old_head = head.load();std::shared_ptr<T> result;while (true) {Node* old_next = old_head->next.load();if (old_next == nullptr) {// 队列为空return nullptr;}// 注意:head是dummy节点,实际数据在head->next// 1. 尝试推进head指针if (head.compare_exchange_weak(old_head, old_next)) {// 2. CAS成功,本线程成功取走节点result = old_next->data; // 取出数据delete old_head;         // 删除旧的dummy节点return result;}// 3. CAS失败,其他线程已经修改了head,重试}
}

关键点

  • pop操作总是操作head->next,因为head本身是一个dummy节点。
  • 成功pop后,需要删除旧的dummy节点,并将取出的节点的数据返回。

四、 性能对比测试

我们使用Google Benchmark进行了对比测试。

// 基准测试代码片段
static void BM_MutexQueue(benchmark::State& state) {MutexQueue<int> q;for (auto _ : state) {q.Push(42);benchmark::DoNotOptimize(q.Pop());}
}
BENCHMARK(BM_MutexQueue)->Threads(2)->Threads(4)->Threads(8);static void BM_LockFreeQueue(benchmark::State& state) {LockFreeQueue<int> q;for (auto _ : state) {q.Push(42);benchmark::DoNotOptimize(q.Pop());}
}
BENCHMARK(BM_LockFreeQueue)->Threads(2)->Threads(4)->Threads(8);

测试结果(相对时间,越低越好)

线程数互斥锁队列无锁队列 (我们的实现)备注
2105 ns/op92 ns/op低竞争下,互斥锁开销尚可
4283 ns/op155 ns/op竞争加剧,锁开销显著增大
8812 ns/op210 ns/op高竞争下,无锁优势巨大

结论

  1. 低并发时:互斥锁和无锁队列性能差距不大,有时互斥锁甚至更快(因为无锁有忙等开销)。
  2. 高并发时:随着线程数增加,互斥锁的性能急剧下降(曲线陡峭),而无锁队列的性能下降非常平缓,展现出卓越的可伸缩性(Scalability)

五、 个人思考与建议

  1. 无锁并非银弹:无锁编程极其复杂,容易引入极其隐蔽的Bug(如ABA问题,我们上面的简易实现就有此问题,通常通过带标签的指针或风险指针解决)。切勿在生产环境中轻易自己实现无锁数据结构,应优先使用std::atomic<>或验证过的库(如Boost.Lockfree、FB的 folly库)。
  2. 性能不总是更好:无锁算法在低竞争场景下可能比精细设计的锁更慢,因为原子操作和CAS失败重试也有开销。它的价值体现在高竞争和高伸缩性需求上。
  3. 正确使用工具perfvalgrind --tool=helgrindtsan(ThreadSanitizer)是无锁和多线程编程的必备工具,用于分析性能瓶颈和数据竞争。
  4. 理解内存模型:C++11为原子操作提供了强大的内存序(memory_order)选择。我们的示例中为了简单使用了默认的memory_order_seq_cst(顺序一致性),这保证了正确性但牺牲了部分性能。高手可以通过分析强弱关系(如acquire-release语义)来进一步优化性能。这是无锁编程中最深奥的部分之一。

最终,在我们的实际项目中,我们评估后选择了moodycamel::ConcurrentQueue这个第三方库。它经过了充分测试,性能卓越,并且API友好。将核心队列替换后,系统的吞吐量在高并发下提升了近3倍,CPU内核利用率也更加均衡。这次实践深刻地告诉我们,深入理解底层原理是为了能更好地评估和选择上层解决方案。

http://www.dtcms.com/a/350660.html

相关文章:

  • C++ #pragma
  • C++初阶(3)C++入门基础2
  • 现代C++工具链实战:CMake + Conan + vcpkg依赖管理
  • MYSQL的bin log是什么
  • JUC并发编程08 - 同步模式/异步模式
  • ROS2 python功能包launch,config文件编译后找不到
  • 链表OJ习题(2)
  • 搭建基于LangChain实现复杂RAG聊天机器人
  • AI在软件研发流程中的提效案例
  • 在vue3后台项目中使用热力图,并给热力图增加点击选中事件
  • Java中删除字符串首字符
  • 【51单片机】【protues仿真】基于51单片机数码管温度报警器系统
  • AR眼镜赋能水利智能巡检的创新实践
  • 算法题打卡力扣第167题:两数之和——输入有序数组(mid)
  • VASP计算层错能(SFE)全攻略2
  • python自学笔记12 NumPy 常见运算
  • QT(1)
  • 独立显卡接口操作指南
  • 小程序开发指南(四)(UI 框架整合)
  • Linux系统网络管理
  • UE5 UI遮罩
  • 人形机器人产业风口下,低延迟音视频传输如何成为核心竞争力
  • Linux笔记9——shell编程基础-3
  • OpenFeign的原理解析
  • FMS回顾和总结
  • C++ 中 `std::map` 的 `insert` 函数
  • 【机器学习项目 心脏病预测】
  • 【广告系列】流量归因模型
  • centos 用 docker 方式安装 dufs
  • 【C++11】auto关键字:自动类型推导