C++11:原子操作与内存顺序:从理论到实践的无锁并发实现

文章目录

    • 0.简介
    • 1.并发编程需要保证的特性
    • 2.原子操作
      • 2.1 原子操作的特性
    • 3.内存顺序
      • 3.1 顺序一致性
      • 3.2 释放-获取(Release-Acquire)
      • 3.3 宽松顺序(Relaxed)
      • 3.4 内存顺序
    • 4.无锁并发
    • 5. 使用建议

0.简介

在并发编程中,原子性、可见性和有序性是确保程序正确执行的三大特性。常见的保证这三个特性的操作是通过加锁来限制资源的访问,但这种方式会带来性能的降低,所以无锁编程变的日益常见,本文将对原子性、可见性和有序性进行介绍,同时介绍原子操作和内存顺序从而实现无锁的并发。

1.并发编程需要保证的特性

要理解原子性,可见性和有序性就需要先明确其对应问题,先从硬件架构和内存访问来看,现代的处理器为了提高性能,普遍采用的都是多级缓存技术和乱序执行技术。
1)多级缓存:每个cpu核心拥有独立的L1/L2缓存,共享L3缓存。这导致不同核心间可见性延迟,如对同一个变量的读写可能存在问题。
2)乱序执行:为了充分的利用指令流水线,只要不影响单线程内结果,指令是存在重排的可能性的,这就导致多线程下可能存在问题。
了解了面对的基础设施,接下来来看这三个特性如果不能保证会存在的问题:
1)原子性:其保证一次操作是原子的,比如var = var +1,假设不是原子的将其分为三步,取值,增加,写回,此时如果有两个线程分别都循环一百次增加var操作,这样线程间就可能互相取到尚未写回(也就是增加了但是没有回)的数值,此时在这基础上增加,就会导致未写回的增加丢失,从而导致结果错误,可以参考下图:
在这里插入图片描述

2)可见性:由于上面说的cpu多级缓存,可能存在没有同步到就读取的情况,比如通过bool值判断是否停止循环,这个bool在其他线程设置,此时就可能导致循环无法停止,cpu占用高。
3)有序性:是指执行的指令按照正常顺序执行不会因为重排带来问题,比如一个线程初始化数据,另一个线程使用,根据变量判断是否初始化完成,重排后可能存在问题,可以参考下面,假设这俩函数分别被两个线程执行,此时对于thread2来说,两个语句的先后不影响本身(也就是两个语句没有依赖关系),可能将bInit=true放到前面执行,a=new char[10]放到后面执行,此时thread1就可能出现使用到空指针情况。

char* a = nullptr;
int bInit = 0;
void thread1()
{if(bInit){//使用a}
}
void thread2()
{a = new char[10];bInit = true;
}

上面三个特性都可以通过加锁保证,但是锁内容不是本文主题,下面将描述原子操作和内存顺序如何保证这三个特性,从而实现无锁的编程。

2.原子操作

C++11引入了头文件,提供标准的原子类型和操作,下面是两个线程对于一个变量循环加一的操作例子:

#include <thread>
#include <iostream>
std::atomic<int> counter(0);  // 原子整型
void worker() {for (int i = 0; i < 1000; ++i) {counter.fetch_add(1, std::memory_order_relaxed);  // 原子加1}
}
int main() {std::thread t1(worker);std::thread t2(worker);t1.join();t2.join();std::cout << "Final counter value: " << counter << std::endl;  // 输出2000
}

2.1 原子操作的特性

1)不可分割性:原子操作不会被其他线程中断。
2)内存可见性:操作结果按指定的内存顺序规则对其他线程可见。
2.2 原子操作的分类
1)读操作:采用load()
2)写操作:采用store()
3)修改操作:fetch_add()、fetch_sub()、compare_exchange_strong() 等
每种操作都可以指定内存顺序参数,控制操作可见性和有序性。

3.内存顺序

原子操作本身保证原子性后,可见性和有序性可以通过指定其内存顺序参数来保证,主要可以分为三类:

3.1 顺序一致性

顺序一致性比较好理解,所有的线程看到的原子操作顺序都相同,其执行顺序清晰且可以预测,但其性能开销较大,例子如下:

std::atomic<int> x(0), y(0);
void thread1() {x.store(1, std::memory_order_seq_cst);  // 写xy.store(1, std::memory_order_seq_cst);  // 写y
}
void thread2() {while (y.load(std::memory_order_seq_cst) == 0);  // 等待y=1// 下面使用x一定是1
}

3.2 释放-获取(Release-Acquire)

释放-获取操作用于保证其调用前后的顺序。
1)释放操作(store+release):确保之前的写操作(如data=42)对其他线程可见。
2)获取操作(load+acquire):确保后续所有读操作(如使用data的地方)能看到释放操作前的所有写。
可以简单理解就是释放操作前的写操作不能重排到释放操作之后,获取操作后的读操作不能重排序到获取操作前。其可以用于像生产者消费者模型,锁机制等。

std::atomic<bool> ready(false);
int data = 0;
void producer() {data = 42;                                // 操作1ready.store(true, std::memory_order_release);  // 释放操作(操作2)
}
void consumer() {while (!ready.load(std::memory_order_acquire));  // 获取操作(操作3)if(data == 42)   xxx;  //一定为true
}

3.3 宽松顺序(Relaxed)

其特点是只保证原子性,不去保证顺序和可见性,性能开销最小,适用于无关顺序的场景,如计数器等。

std::atomic<int> counter(0);
void increment() {counter.fetch_add(1, std::memory_order_relaxed);
}

3.4 内存顺序

除了上述基于原子操作的内存顺序,C++还显式的提供了内存屏障,其主要类型如下:
1)释放屏障:确保对于屏障前的所有写操作不会重排到屏障后。
2)获取屏障:防止屏障后的操作重排到屏障前。
3)全屏障:具有释放和获取的特性。

std::atomic<int> x(0), y(0);
void thread1() {x.store(1, std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_release);  // 释放屏障y.store(1, std::memory_order_relaxed);
}
void thread2() {while (y.load(std::memory_order_relaxed) == 0) {}std::atomic_thread_fence(std::memory_order_acquire);  // 获取屏障//x一定为1
}

4.无锁并发

有了上面的了解,可以来尝试实现一个无锁的队列,主要利用原子操作和内存顺序,去保证其原子性,可见性以及有序性,下面是一个简单的例子,可以参考。

#include <atomic>
#include <memory>
#include <iostream>
template<typename T>
class LockFreeQueue {
private:// 队列节点结构struct Node {T data;                      // 节点数据std::atomic<Node*> next;     // 指向下一个节点的原子指针Node(const T& value) : data(value), next(nullptr) {}};std::atomic<Node*> head;         // 队头指针std::atomic<Node*> tail;         // 队尾指针
public:// 构造函数:初始化队头和队尾指针指向一个虚拟节点LockFreeQueue() {Node* dummy = new Node(T());  // 创建虚拟节点head.store(dummy, std::memory_order_relaxed);tail.store(dummy, std::memory_order_relaxed);}// 析构函数:释放队列中所有节点的内存~LockFreeQueue() {while (head.load() != nullptr) {Node* oldHead = head.load();head.store(oldHead->next.load(), std::memory_order_relaxed);delete oldHead;}}// 入队操作void enqueue(const T& value) {Node* newNode = new Node(value);  // 创建新节点Node* oldTail = tail.load(std::memory_order_relaxed);// 使用 CAS 循环尝试将新节点添加到队尾while (true) {// 先尝试更新 tail 的 next 指针if (oldTail->next.compare_exchange_weak(nullptr, newNode,std::memory_order_release,  // 释放语义:确保 newNode 的数据对其他线程可见std::memory_order_relaxed)) {// CAS 成功,更新 tail 指针指向新节点tail.compare_exchange_strong(oldTail, newNode,std::memory_order_relaxed,std::memory_order_relaxed);return;}// CAS 失败,说明其他线程已经更新了 tail->next// 更新 oldTail 为最新的 tail 值并重试oldTail = tail.load(std::memory_order_relaxed);}}// 出队操作bool dequeue(T& value) {Node* oldHead = head.load(std::memory_order_relaxed);// 使用 CAS 循环尝试出队while (true) {if (oldHead == tail.load(std::memory_order_relaxed)) {// 队列为空(只有虚拟节点)return false;}Node* nextNode = oldHead->next.load(std::memory_order_acquire);// 获取语义:确保能看到入队线程设置的 newNode 的数据// 尝试更新 head 指针if (head.compare_exchange_weak(oldHead, nextNode,std::memory_order_relaxed,std::memory_order_relaxed)) {// 获取数据(跳过虚拟节点)value = nextNode->data;delete oldHead;  // 释放原头节点(虚拟节点或旧数据节点)return true;}// CAS 失败,说明其他线程已经更新了 head// 更新 oldHead 为最新的 head 值并重试oldHead = head.load(std::memory_order_relaxed);}}// 检查队列是否为空bool empty() const {return head.load(std::memory_order_relaxed) == tail.load(std::memory_order_relaxed);}
};

5. 使用建议

无锁的编程虽然在一定程度上提高了性能,但其也带来了复杂性和问题排查的困难,可以在对性能有要求且有足够测试的场景下使用。

原文链接:https://mp.weixin.qq.com/s/thnlXKZnKE4foxZ5Vi3NYQ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/908993.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle 归档日志与RECOVERY_FILE_DEST 视图

1. RECOVERY_FILE_DEST 视图的作用 RECOVERY_FILE_DEST 是 Oracle 数据库用于 管理快速恢复区&#xff08;Fast Recovery Area, FRA&#xff09; 的一个视图。FRA 是 Oracle 提供的一种集中存储恢复相关文件&#xff08;如归档日志、备份文件、闪回日志等&#xff09;的区域。…

零基础玩转物联网-串口转以太网模块如何快速实现与MQTT服务器通信

目录 1 前言 2 环境搭建 2.1 硬件准备 2.2 软件准备 2.3 驱动检查 3 MQTT服务器通信配置与交互 3.1 硬件连接 3.2 开启MQTT服务器 3.3 打开配置工具读取基本信息 3.4 填写连接参数进行连接 3.5 通信测试 4 总结 1 前言 MQTT&#xff1a;全称为消息队列遥测传输协议&#xff08;…

六、Sqoop 导出

作者&#xff1a;IvanCodes 日期&#xff1a;2025年6月7日 专栏&#xff1a;Sqoop教程 Apache Sqoop 不仅擅长从关系型数据库 (RDBMS) 向 Hadoop (HDFS, Hive, HBase) 导入数据&#xff0c;同样也强大地支持反向操作——将存储在 Hadoop 中的数据导出 (Export) 回关系型数据库。…

数据结构-如果将堆结构应用到TOP-K问题上会怎样?

数据结构的应用-如何用堆解决TOP-K问题 前言一、TOP-K问题是什么&#xff1f;二、如何用堆解决TOP-K问题1.怎么建堆&#xff0c;建大堆还是小堆&#xff1f;2.代码实现 总结 前言 本篇文章进行如何用堆结构解决TOP-K问题的讲解 一、TOP-K问题是什么&#xff1f; TOP-k问题&am…

Elasticsearch的索引

正向索引和倒排索引 什么是正向索引&#xff1f; 传统的数据库采用正向索引&#xff0c;如MySQL将表中的id创建索引&#xff0c;正向索引在进行不是id为索引进行搜索的时候&#xff0c;会逐条进行查询&#xff0c;比方说 上图的表格&#xff0c;数据库进行逐条查询&#xff0c;…

分散电站,集中掌控,安科瑞光伏云平台助力企业绿色转型

本项目位于香港全境共计52个分布式光伏站&#xff0c;总装机容量8.6MW。发电模式自发自用&#xff0c;余电上网&#xff0c;逆变器采用阳光电源SG100CX、SG20RT等12种型号共计103台&#xff0c;其余型号共计15台。每个站点均配置气象站。 项目采用AcrelCloud-1200分布式光伏运…

开发记录:修复一些Bug,并实现两个功能

开发记录&#xff1a; &#x1f4cb; 工作概述 到今天主要完成了AI阅读助手的两大核心功能&#xff1a;前情提要和名词解释&#xff0c;并对相关交互体验进行了优化。通过流式SSE技术实现了实时AI内容生成&#xff0c;大幅提升了用户体验。 &#x1f3af; 主要完成功能 1…

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…

【HarmonyOS 5.0】开发实战:从UI到Native全解析

一、环境搭建与项目创建 ​​跨平台安装​​ DevEco Studio支持Windows/macOS系统&#xff0c;安装包集成HarmonyOS SDK、Node.js和OHPM工具链。 Windows&#xff1a;双击.exe选择非中文路径macOS&#xff1a;拖拽.app至Applications目录验证&#xff1a;通过Help > Diagnos…

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程

STM32F1 本教程使用零知标准板&#xff08;STM32F103RBT6&#xff09;通过I2C驱动ICM20948九轴传感器&#xff0c;实现姿态解算&#xff0c;并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化&#xff0c;适合嵌入式及物联网开发者。在基础驱动上新增…

华为OD最新机试真题-食堂供餐-OD统一考试(B卷)

题目描述 某公司员工食堂以盒饭方式供餐。 为将员工取餐排队时间降低为0,食堂的供餐速度必须要足够快,现在需要根据以往员工取餐的统计信息,计算出一个刚好能达成排队时间为0的最低供餐速度。即,食堂在每个单位时间内必须至少做出 多少价盒饭才能满足要求。 输入描述 第1行…

【笔记】MSYS2 的 MINGW64 环境 全面工具链

#工作记录 MSYS2 的 MINGW64 环境&#xff08;mingw64.exe&#xff09;&#xff0c;下面是为该环境准备的最全工具链安装命令&#xff08;包括 C/C、Python、pip/wheel、GTK3/GTK4、PyGObject、Cairo、SDL2 等&#xff09;。 这一环境适用于构建原生 64 位 Windows 应用程序。…

基于 HTTP 的单向流式通信协议SSE详解

SSE&#xff08;Server-Sent Events&#xff09;详解 &#x1f9e0; 什么是 SSE&#xff1f; SSE&#xff08;Server-Sent Events&#xff09; 是 HTML5 标准中定义的一种通信机制&#xff0c;它允许服务器主动将事件推送给客户端&#xff08;浏览器&#xff09;。与传统的 H…

【react+antd+vite】优雅的引入svg和阿里巴巴图标

1.安装相关包 由于是vite项目&#xff0c;要安装插件来帮助svg文件引入进来&#xff0c;否则会失败 npm下载包 npm i vite-plugin-svgr vite.config.ts文件内&#xff1a; import svgr from "vite-plugin-svgr"; //... export default defineConfig({plugins: …

UI框架-通知组件

UI框架-通知组件 介绍 一个基于 Vue 3 的轻量级通知组件库&#xff0c;提供了丰富的消息通知功能。支持多种通知类型、自定义样式、进度条显示等特性。 特性 &#x1f3a8; 支持多种通知类型&#xff1a;信息、成功、警告、错误⏳ 支持进度条显示&#x1f504; 支持加载中状…

WordZero:让Markdown与Word文档自由转换的Golang利器

在日常工作中&#xff0c;我们经常需要在Markdown和Word文档之间进行转换。Markdown方便编写和版本控制&#xff0c;而Word文档更适合正式的商务环境。作为一名Golang开发者&#xff0c;我开发了WordZero这个库&#xff0c;专门解决这个痛点。 项目背景 GitHub仓库&#xff1…

计算机网络面试汇总(完整版)

基础 1.说下计算机网络体系结构 计算机网络体系结构&#xff0c;一般有三种&#xff1a;OSI 七层模型、TCP/IP 四层模型、五层结构。 简单说&#xff0c;OSI是一个理论上的网络通信模型&#xff0c;TCP/IP是实际上的网络通信模型&#xff0c;五层结构就是为了介绍网络原理而折…

动端React表格组件:支持合并

前言 在移动端开发中&#xff0c;表格组件是一个常见但复杂的需求。相比PC端&#xff0c;移动端表格面临着屏幕空间有限、交互方式不同、性能要求更高等挑战。本文将详细介绍如何从零开始构建一个功能完整的移动端React表格组件&#xff0c;包含固定列、智能单元格合并、排序等…

广告系统中后链路数据为什么要使用流批一体技术?流批一体技术是什么?

在大规模广告系统的后链路(离线和实时特征计算、模型训练与上线、效果监控等)中,往往既有对海量历史数据的批量计算需求(离线特征、离线模型训练、报表汇总),又有对在线请求的低延迟实时计算需求(实时特征、在线打分、实时监控/告警)。传统将二者割裂、用 Lambda 架构…

6.10 - 常用 SQL 语句以及知识点

MySQL 技术 SQL 是结构化查询语言&#xff0c;他是关系型数据库的通用语言 SQL 可以分为分为以下三个类别 DDL (data definition languages) 语句 数据定义语言&#xff0c;定义了 不同的数据库、表、索引等数据库对象的定义。常用的的语句关键字包括 **create、drop、alter …