现代C++:内存模型和atomic:理解并发的复杂性
上一讲我们讨论了一些并发编程的基本概念今天我们来讨论一个略有点绕的问题C 里的内存模型和原子量。C98 的执行顺序问题C98 的年代里开发者们已经了解了线程的概念但 C 的标准里则完全没有提到线程。从实践上估计大家觉得不提线程C 也一样能实现多线程的应用程序吧。不过很多聪明人都忽略了下面的事实可能会产生不符合直觉预期的结果为了优化的必要编译器是可以调整代码的执行顺序的。唯一的要求是程序的“可观测”外部行为是一致的。处理器也会对代码的执行顺序进行调整所谓的 CPU 乱序执行。在单处理器的情况下这种乱序无法被程序观察到但在多处理器的情况下在另外一个处理器上运行的另一个线程就可能会察觉到这种不同顺序的后果了。对于上面的后一点大部分开发者并没有意识到。原因有好几个方面多处理器的系统在那时还不常见主流的 x86 体系架构仍保持着较严格的内存访问顺序只有在数据竞争data race激烈的情况下才能看到“意外”的后果举一个例子假设我们有两个全局变量int x 0; int y 0;然后我们在一个线程里执行x 1; y 2;在另一个线程里执行if (y 2) { x 3; y 4; }想一下你认为上面的代码运行完之后x、y 的数值有几种可能你如果认为有两种可能1、2 和 3、4 的话那说明你是按典型程序员的思维模式看问题的——没有像编译器和处理器一样处理问题。事实上1、4 也是一种结果的可能。有两个基本的原因可以造成这一后果编译器没有义务一定按代码里给出的顺序产生代码。事实上跟据上下文调整代码的执行顺序使其最有利于处理器的架构是优化中很重要的一步。就单个线程而言先执行 x 1 还是先执行 y 2 完全是件无关紧要的事它们没有外部“可观察”的区别。在多处理器架构中各个处理器可能存在缓存不一致性问题。取决于具体的处理器类型、缓存策略和变量地址对变量 y 的写入有可能先反映到主内存中去。之所以这个问题似乎并不常见是因为常见的 x86 和 x86-64 处理器是在顺序执行方面做得最保守的——大部分其他处理器如 ARM、DEC Alpha、PA-RISC、IBM Power、IBM z 架构和 Intel Itanium 在内存序问题上都比较“松散”。x86 使用的内存模型基本提供了顺序一致性sequential consistency相对的ARM 使用的内存模型就只是松散一致性relaxed consistency。较为严格的描述请查看参考资料 [1] 和里面提供的进一步资料。虽说 Intel 架构处理器的顺序一致性比较好但在多处理器包括多核的情况下仍然能够出现写读序列变成读写序列的情况产生意料之外的后果。参考资料中提供了完整的例子包括示例代码。对于缓存不一致性问题的一般中文介绍可以查看参考资料。双重检查锁定在多线程可能对同一个单件进行初始化的情况下有一个双重检查锁定的技巧可基本示意如下// 头文件 class singleton { public: static singleton* instance(); … private: static singleton* inst_ptr_; }; // 实现文件 singleton* singleton::inst_ptr_ nullptr; singleton* singleton::instance() { if (inst_ptr_ nullptr) { lock_guard lock; // 加锁 if (inst_ptr_ nullptr) { inst_ptr_ new singleton(); } } return inst_ptr_; }这个代码的目的是消除大部分执行路径上的加锁开销。原本的意图是如果 inst_ptr_ 没有被初始化执行才会进入加锁的路径防止单件被构造多次如果 inst_ptr_ 已经被初始化那它就会被直接返回不会产生额外的开销。虽然看上去很美但它一样有着上面提到的问题。Scott Meyers 和 Andrei Alexandrecu 详尽地分析了这个用法 [4]然后得出结论即使花上再大的力气这个用法仍然有着非常多的难以填补的漏洞。本质上还是上面说的优化编译器会努力击败你试图想防止优化的努力而多处理器会以令人意外的方式让代码走到错误的执行路径上去。他们分析得非常详细建议你可以花时间学习一下。volatile在某些编译器里使用 volatile 关键字可以达到内存同步的效果。但我们必须记住这不是 volatile 的设计意图也不能通用地达到内存同步的效果。volatile 的语义只是防止编译器“优化”掉对内存的读写而已。它的合适用法目前主要是用来读写映射到内存地址上的 I/O 操作。由于 volatile 不能在多处理器的环境下确保多个线程能看到同样顺序的数据变化在今天的通用应用程序中不应该再看到 volatile 的出现。C11 的内存模型为了从根本上消除这些漏洞C11 里引入了适合多线程的内存模型。跟我们开发密切相关的是现在我们有了原子对象atomic和使用原子对象的获得acquire、释放release语义可以真正精确地控制内存访问的顺序性保证我们需要的内存序。内存屏障和获得、释放语义拿刚才的那个例子来说如果我们希望结果只能是 1、2 或 3、4即满足程序员心中的完全存储序total store ordering我们需要在 x 1 和 y 2 两句语句之间加入内存屏障禁止这两句语句交换顺序。我们在此种情况下最常用的两个概念是“获得”和“释放”获得是一个对内存的读操作当前线程的任何后面的读写操作都不允许重排到这个操作的前面去。释放是一个对内存的写操作当前线程的任何前面的读写操作都不允许重排到这个操作的后面去。具体到我们上面的第一个例子我们需要把 y 声明成 atomic。然后我们在线程 1 需要使用释放语义x 1; y.store(2, memory_order_release);在线程 2 我们对 y 的读取应当使用获得语义但存储只需要松散内存序即可if (y.load(memory_order_acquire) 2) { x 3; y.store(4, memory_order_relaxed); }我们可以用下图示意一下每一边的代码都不允许重排越过黄色区域且如果 y 上的释放早于 y 上的获取的话释放前对内存的修改都在另一个线程的获取操作后可见事实上在我们把 y 改成 atomic 之后两个线程的代码一行不改执行结果都会是符合我们的期望的。因为 atomic 变量的写操作缺省就是释放语义读操作缺省就是获得语义不严格的说法精确表述见下面的内存序部分。即y 2 相当于 y.store(2, memory_order_release)y 2 相当于 y.load(memory_order_acquire) 2但是缺省行为可能是对性能不利的我们并不需要在任何情况下都保证操作的顺序性。另外我们应当注意一下acquire 和 release 通常都是配对出现的目的是保证如果对同一个原子对象的 release 发生在 acquire 之前的话release 之前发生的内存修改能够被 acquire 之后的内存读取全部看到。atomic刚才是对 atomic 用法的一个非正式介绍。下面我们对 atomic 做一个稍完整些的说明。C11 在 头文件中引入了 atomic 模板对原子对象进行了封装。我们可以将其应用到任何类型上去。当然对于不同的类型效果还是有所不同的对于整型量和指针等简单类型通常结果是无锁的原子对象而对于另外一些类型比如 64 位机器上大小不是 1、2、4、8有些平台 / 编译器也支持对更大的数据进行无锁原子操作的类型编译器会自动为这些原子对象的操作加上锁。编译器提供了一个原子对象的成员函数 is_lock_free可以检查这个原子对象上的操作是否是无锁的。原子操作有三类读在读取的过程中读取位置的内容不会发生任何变动。写在写入的过程中其他执行线程不会看到部分写入的结果。读‐修改‐写读取内存、修改数值、然后写回内存整个操作的过程中间不会有其他写入操作插入其他执行线程不会看到部分写入的结果。头文件中还定义了内存序分别是memory_order_relaxed松散内存序只用来保证对原子对象的操作是原子的memory_order_consume目前不鼓励使用我就不说明了memory_order_acquire获得操作在读取某原子对象时当前线程的任何后面的读写操作都不允许重排到这个操作的前面去并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见memory_order_release释放操作在写入某原子对象时当前线程的任何前面的读写操作都不允许重排到这个操作的后面去并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见memory_order_acq_rel获得释放操作一个读‐修改‐写操作同时具有获得语义和释放语义即它前后的任何读写操作都不允许重排并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见memory_order_seq_cst顺序一致性语义对于读操作相当于获取对于写操作相当于释放对于读‐修改‐写操作相当于获得释放是所有原子操作的默认内存序除此之外顺序一致性还保证了多个原子量的修改在所有线程里观察到的修改顺序都相同我们目前的讨论暂不涉及多个原子量的修改atomic 有下面这些常用的成员函数默认构造函数只支持零初始化拷贝构造函数被删除使用内置对象类型的构造函数不是原子操作可以从内置对象类型赋值到原子对象相当于 store可以从原子对象隐式转换成内置对象相当于 loadstore写入对象到原子对象里第二个可选参数是内存序类型load从原子对象读取内置对象有个可选参数是内存序类型is_lock_free判断对原子对象的操作是否无锁是否可以用处理器的指令直接完成原子操作exchange交换操作第二个可选参数是内存序类型这是读‐修改‐写操作compare_exchange_weak 和 compare_exchange_strong两个比较加交换CAS的版本你可以分别指定成功和失败时的内存序也可以只指定一个或使用默认的最安全内存序这是读‐修改‐写操作fetch_add 和 fetch_sub仅对整数和指针内置对象有效对目标原子对象执行加或减操作返回其原始值第二个可选参数是内存序类型这是读‐修改‐写操作 和 --前置和后置仅对整数和指针内置对象有效对目标原子对象执行增一或减一操作使用顺序一致性语义并注意返回的不是原子对象的引用这是读‐修改‐写操作 和 -仅对整数和指针内置对象有效对目标原子对象执行加或减操作返回操作之后的数值操作使用顺序一致性语义并注意返回的不是原子对象的引用这是读‐修改‐写操作有了原子对象之后我们可以轻而易举地把之前讲过的shared_count 变成线程安全。我们只需要包含 头文件并把下面这行long count_;修改成std::atomic_long count_;即可atomic_long 是 atomic 的类型别名。不过由于我们并不需要 之后计数值影响其他行为在 add_count 中执行简单的 、使用顺序一致性语义略有浪费。更好的做法是将其实现成void add_count() noexcept { count_.fetch_add( 1, std::memory_order_relaxed); }is_lock_free 的可能问题注意macOS 上在使用 Clang 时似乎不支持对需要加锁的对象使用 is_lock_free 成员函数此时链接会出错。而 GCC 在这种情况下需要确保系统上装了 libatomic。以 CentOS 7 下的 GCC 7 为例我们可以使用下面的语句来安装sudo yum install devtoolset-7-libatomic-devel然后用下面的语句编译可以通过g -pthread test.cpp -latomicWindows 下使用 MSVC 则没有问题。mutex上一讲我们已经讨论了互斥量。今天我们只需要补充两点互斥量的加锁操作lock具有获得语义互斥量的解锁操作unlock具有释放语义有了目前讲过的这些知识我们终于可以实现一个真正安全的双重检查锁定了// 头文件 class singleton { public: static singleton* instance(); … private: static mutex lock_; static atomicsingleton* inst_ptr_; }; // 实现文件 mutex singleton::lock_; atomicsingleton* singleton::inst_ptr_; singleton* singleton::instance() { singleton* ptr inst_ptr_.load( memory_order_acquire); if (ptr nullptr) { lock_guardmutex guard{lock_}; ptr inst_ptr_.load( memory_order_relaxed); if (ptr nullptr) { ptr new singleton(); inst_ptr_.store( ptr, memory_order_release); } } return inst_ptr_; }并发队列的接口在结束这一讲之前我们来检查一下并发对编程接口的冲击。回想我们之前讲到标准库里 queue 有下面这样的接口template typename T class queue { public: … T front(); const T front() const; void pop(); … }我们之前还问过为什么 pop 不直接返回第一个元素。可到了并发的年代我们不禁要问这样的接口设计到底明智吗会不会在我们正在访问 front() 的时候这个元素就被 pop 掉了事实上上面这样的接口是不可能做到并发安全的。并发安全的接口大概长下面这个样子template typename T class queue { public: … T front(); const T front() const; void pop(); … }我们之前还问过为什么 pop 不直接返回第一个元素。可到了并发的年代我们不禁要问这样的接口设计到底明智吗会不会在我们正在访问 front() 的时候这个元素就被 pop 掉了事实上上面这样的接口是不可能做到并发安全的。并发安全的接口大概长下面这个样子template typename T class queue { public: … void wait_and_pop(T dest) bool try_pop(T dest); … }换句话说要准备好位置去接收然后如果接收成功了才安安静静地在自己的线程里处理已经被弹出队列的对象。接收方式还得分两种阻塞式的和非阻塞式的……那我为什么要在内存模型和原子量这一讲里讨论这个问题呢因为并发队列的实现经常是用原子量来达到无锁和高性能的。单生产者、单消费者的并发队列用原子量和获得、释放语义就能简单实现。对于多生产者或多消费者的情况那实现就比较复杂了一般会使用 compare_exchange_strong 或 compare_exchange_weak。讨论这个话题的复杂性就大大超出了本专栏的范围了。你如果感兴趣的话可以查看下面几项内容nvwa::fc_queue 给出了一个单生产者、单消费者的无锁并发定长环形队列代码长度是几百行的量级。moodycamel::ConcurrentQueue 给出了一个多生产者、多消费者的无锁通用并发队列代码长度是几千行的量级。陈皓给出了一篇很棒的对无锁队列的中文描述推荐阅读。内容小结在这一讲里我们讨论了 C 对并发的底层支持特别是内存模型和原子量。这些底层概念是在 C 里写出高性能并发代码的基础。

相关新闻

计算机毕业设计之基于springboot的社区管理系统的设计与实现

计算机毕业设计之基于springboot的社区管理系统的设计与实现

随着社会的发展,系统的管理形势越来越严峻。越来越多的用户利用互联网获得信息,但各种信息鱼龙混杂,信息真假难以辨别。为了方便用户更好的获得社区信息,因此,设计一种安全高效的社区管理系统极为重要。为设计一个安全…

2026/10/13 2:06:34 阅读更多 →
Unity UI事件系统深度解析:自定义InputModule与EventTrigger实战

Unity UI事件系统深度解析:自定义InputModule与EventTrigger实战

1. 项目概述:为什么Unity的UI交互总在关键时刻“掉链子”?如果你在Unity里做过稍微复杂一点的UI交互,比如一个带拖拽、长按、双击确认的背包系统,或者一个需要精确响应滑动和点击的虚拟摇杆,那你大概率遇到过这种场景&…

2026/10/13 3:13:54 阅读更多 →
掌握这3个AI技术方掌握这3个AI技术方法,效率提升100%

掌握这3个AI技术方掌握这3个AI技术方法,效率提升100%

深入解析:🎯 实践挑战:AI专业分数普涨的数据洞察与趋势预测 在系统架构设计中,— 技术要点1:🎯 实践挑战:AI专业分数普涨的数据洞察与趋势预测 任务描述: 收集近三年AI专业录取数据&…

2026/10/14 10:00:01 阅读更多 →

最新新闻

Wechaty 集成 wechatferry:用 @wechatferry/puppet 实现 PC Hook 微信机器人的完整实践

Wechaty 集成 wechatferry:用 @wechatferry/puppet 实现 PC Hook 微信机器人的完整实践

即时通讯后端AI 应用 【免费下载链接】wechatferry - 项目地址: https://gitcode.com/gh_mirrors/wec/wechatferry 点击查看 免费下载 本文围绕 wechatferry 仓库中的 Wechaty 集成文档 docs/integrations/wechaty.md 展开,讲解如何通过 wechatferry/pu…

2026/10/14 17:38:02 阅读更多 →
Web前端MOOC测试题:HTML/CSS/JavaScript考点与答题策略

Web前端MOOC测试题:HTML/CSS/JavaScript考点与答题策略

1. 这里不是“背答案”那么简单:先看这门课的测试题到底在考什么先纠正个笔误——标题里写的“前段开发”,应该是“前端开发”。这不是抠字眼,是因为我在给学生答疑时发现,很多人在搜索引擎里输入的关键词本身就带着错别字&#x…

2026/10/14 17:38:02 阅读更多 →
STM32 F103C8T6驱动AHT10温湿度传感器:模拟I2C实现与避坑指南

STM32 F103C8T6驱动AHT10温湿度传感器:模拟I2C实现与避坑指南

简介:面向嵌入式初学者的STM32 F103C8T6学习笔记工程包,主题是AHT10温湿度传感器驱动与数据采集。内容围绕I2C(集成电路总线)通信展开,涵盖引脚复用配置、库函数收发、传感器初始化与校准、温湿度原始数据移位换算、校…

2026/10/14 17:38:02 阅读更多 →
WhoDB Impeccable 的 shape 命令详解:写代码前的三阶段 UX/UI 规划工作流

WhoDB Impeccable 的 shape 命令详解:写代码前的三阶段 UX/UI 规划工作流

数据库客户端后端前端CLI桌面应用数据可视化AI 应用 【免费下载链接】whodb Where data access meets operational intelligence 项目地址: https://gitcode.com/gh_mirrors/wh/whodb 点击查看 免费下载 在 WhoDB 仓库中,.agents/skills/impeccable/ 目…

2026/10/14 17:38:02 阅读更多 →
字符串的基本知识

字符串的基本知识

字符串的基本知识 #字符串练习/某地区旅游人数 tra_peroson_count 1000 tra_turnover_day 99999.9 #打印数据类型 print(type(tra_peroson_count),type(tra_turnover_day)) python_string1 “继续学习python语言” python_string2 ‘继续学习python语言’ python_string3 …

2026/10/14 17:38:02 阅读更多 →
东方财富行情数据接口抓取实战:从XHR响应到SQLite增量存储

东方财富行情数据接口抓取实战:从XHR响应到SQLite增量存储

做数据的人,十个里有八个都动过爬东方财富网的心思。这个系列第二篇,我不再抓静态页面,而是直接抓行情数据接口。上一篇跑通了最基础的页面解析,但那只是开胃菜:页面源码里看不到的几个关键字段——最新价、涨跌幅、成…

2026/10/14 17:37:01 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →