惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
H
Help Net Security
Recorded Future
Recorded Future
The Register - Security
The Register - Security
F
Full Disclosure
N
Netflix TechBlog - Medium
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
爱范儿
爱范儿
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
I
InfoQ
T
Tenable Blog
T
Tor Project blog
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net
NISL@THU
NISL@THU
Google DeepMind News
Google DeepMind News
博客园 - 叶小钗
B
Blog
V
V2EX
Jina AI
Jina AI
L
LangChain Blog
月光博客
月光博客
W
WeLiveSecurity
U
Unit 42
AWS News Blog
AWS News Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 聂微东
V
Visual Studio Blog
A
Arctic Wolf
T
Tailwind CSS Blog
The Cloudflare Blog
SecWiki News
SecWiki News
S
SegmentFault 最新的问题
Hacker News - Newest:
Hacker News - Newest: "LLM"
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Securelist
www.infosecurity-magazine.com
www.infosecurity-magazine.com
腾讯CDC
雷峰网
雷峰网

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
并发编程系列(二)—— store, load 与 RMW
同勉共进 · 2026-04-27 · via 博客园_首页

背景知识

store buffer 与 L1/L2/L3 cache

注意,store buffer 不是缓存,它属于 CPU 而不是缓存系统。

不同 CPU 都有 store buffer 吗?

现代高性能的超标量处理器几乎都配备了 Store Buffer,可能叫法不同,但都有类似的“东西”和机制。

什么是“写完成”?

store操作完成的标志是什么?如果只是写到 store buffer,那么在架构层面上(Architecturally)对全局依然是“未发生”的,因为store完成的标志是“写入缓存,让别人看见”。

Store-Load 重排

先看一个例子:

初始值:
x = 0, y = 0
thread 1:               thread2:
x.store(1)    -- A       y.store(1)    -- C
a = y.load()  -- B       b = x.load()  -- D

时序:

1. thread 1 对 x 执行写操作,但只是把 1 写入 store buffer
2. 同时,thread 2 对 y 执行写操作,但也只是把 1 写入 store buffer
3. thread 1 在没有把 store buffer 刷入 L1 cache 的前提下,就率先读了 y 的值
  (Store-Load 重排),而此时,thread 2 对 y 的写也还停留在 store buffer,
   对 thread 1 不可见,于是,thread 1 读到的 y 值是 0。
4. 同理,thread 2 读到的 x 值也是 0。
5. thread 1 和 thread 2 把各自的store buffer 刷到了 L1 cache,但为时已晚,因为 load
   先一步执行了,a == 0 && b == 0 的事实已经铸就。

基于代码顺序,有 A --> B 并且 C --> D。

从 thread 1 的视角看,既然它读到了y的旧值,那么有 B--> C。

从 thread 2 的视角看,既然它读到了x的旧值,那么有 D --> A。

从 thread 1 的视角看,总体顺序是 A --> B --> C --> D

从 thread 2 的视角看,总体顺序是 C --> D --> A--> B

即,不同线程看到的全局顺序不一样,并且,两者结合起来,形成了 A--> B --> C -->D --> A 这样的环。

RMW (atomic read-modify-write operation) 和 load 的区别

  1. 从“自我”的视角看,RMW / load 都是从 cache 或 main memory 中读取,至少在读到的那一刻,它们看到的都是最新的值。
  2. 如果在读的一瞬间,其它线程修改了同一个原子变量,但只写到了 store buffer,那么,从架构层面上看,这个“写”并没有完成,所以不算“最新值”。因此,这种情况不影响第1条中“两者都读到最新值”的结论。
  3. 两者的区别在于“对他人的影响”上。
    1. RMW 会先通过 RFO (Read For Ownership) 请求要求其它核心交出最新值并作废自己 cache 中的副本。
    2. 在“读”动作完成后,RMW 会锁住对应的 cache line 直到整个 RMW 操作完成,这期间,别的线程既不能读,更不能写这条 cache line。
    3. 普通 load 只是自己读,不干涉别人,它读的时候别人可能也在读,在它刚读完的一瞬间,别人就可能修改了这个值。

RMW 和 store 的区别

store可能会将数据放入 store buffer,导致延迟可见,但 RMW 一定不会。

  • 当 CPU store某个变量时,如果该变量所在的 Cache Line 不在 L1 Cache 中,或者状态不是 Exclusive / Modified(味着其他核心也缓存了该数据,需要通过总线发送 Invalidate 消息让其他核心作废缓存),CPU 必须向总线发送信号获取该缓存行的所有权。等待缓存行返回需要几百个时钟周期,为了提效,CPU 会把要写入的值暂存进 Store Buffer,然后继续执行下一条指令。此时,这个写操作对其他 CPU 是不可见的,直到 Store Buffer 最终将数据刷入 L1 缓存。这就是普通 store 导致延迟可见的物理根源。
  • RMW 在读取阶段就已经独占了 cache line,写入的数据会直接放入 cache line。RMW 执行完成,释放 cache line 锁的那一刻,数据就全局可见了。

RMW 会产生全内存屏障 (full memory barrier) 吗?

x86 会,ARM 不会。

x86 下,RMW 会被翻译成lock前缀或者隐式lock前缀(就是不显式携带lock但有lock的功能)的指令,例如,x86-64下,fetch_add被翻译成lock xadd指令。无论是显式 lock(比如 lock xadd)还是隐式 lock,都会生成全内存屏障 (Full Memory Barrier),即,在当前 CPU core上:

  • 在 lock 指令执行之前的所有读写操作,必须在 lock 指令执行完毕前完成;
  • 会强制将当前核心的 store buffer 刷入 L1 Cache,确保 lock 之前的读写操作,对其它核心都可见。
  • 在 lock 指令之后的所有读写操作,必须等 lock 指令完成后才能开始。

ARM 采用弱内存模型,硬件有更大的自由度。比如,data.fetch_add(1, std::memory_order_release)会被翻译成ldxrstlxr的组合(详见下文),只具备 release 语义,不具备 acquire 语义。即只能保证 fetch_add前面的指令不会被重排到fetch_add后面,但不能保证fetch_add后面的指令一定不会跑到fetch_add的前面。

full memory barrier 的理解

双向的。即

=========== 前向屏障:前面的指令不能越过 ===========
会产生 full memory barrier 的指令,比如 lock xadd
=========== 后向屏障:后面的指令不能越过 ===========

即:
1. lock xadd 前的指令,不能排到 lock xadd 后面,换言之,在 lock xadd 被 CPU 执行之前,它前面
   的指令,都必须被CPU执行完,且对全局可见。
2. lock xadd 后的指令,不能排到 lock xadd 前面,换言之,在 lock xadd 被 CPU 执行之前,它后面
   的指令,都不能先于它被 CPU 执行。

store/load/ RMW 对应的汇编指令

原子变量以std::atomic<long long> data{0}为例,RMW 操作以fetch_add为例,编译器为 GCC 15.2,通过 Compiler Explorer 来观察生成的汇编代码。

指令 内存序 X86_64 ARM64 ARM64 (-mno-outline-atomics)[1]
store[2] relaxed movq str str
release stlr stlr
seq_cst xchgq
load[3] relaxed movq ldr ldr
acquire ldar ldar
seq_cst
fetch_add relaxed lock xadd __aarch64_ldadd8_relax ldxr & stxr
release __aarch64_ldadd8_rel ldxr & stlxr
acquire __aarch64_ldadd8_acq ldaxr & stxr
acq_rel __aarch64_ldadd8_acq_rel ldaxr & stlxr
seq_cst

备注:

  1. ARM 架构下默认编译成内置函数,加上-mno-outline-atomics参数后,编译成汇编指令。
  2. store只支持这3中内存序,若使用其它内存序,则是未定义行为。
  3. load只支持这3中内存序,若使用其它内存序,则是未定义行为。

x86_64 体系下的store/load/ fetch_add

store(relaxed)store(release)

为什么两者都翻译成movq

mov 指令天然的原子性

x86_64 采用 TSO (Total Store Order) 这种强内存模型,对自然对齐的(32位、64位)内存进行读写,mov 指令(movlmovq等)在硬件层面就保证了原子性。

架构天然的 release 语义

什么是 release 语义?

当前线程内,store操作前的任何读写,包括非原子变量的读写,都不能被重排到该store操作的后面。

为什么天然?

背景:先要搞懂什么是 CPU 的乱序执行(指令重排)。汇编代码中,是指令 A 在前,指令 B 在后,而 CPU 在实际执行是,可能先执行指令 B,后执行指令A,这就是所谓的 CPU 乱序执行。

如前所述,x86_64 采用 TSO 强内存模型,在硬件层面就禁止了下面这两种重排:

  • 禁止 Load-Store 重排:如果汇编代码是 读操作 A --> 写操作 B,那么,CPU在执行时,绝不可能先执行 B 后执行 A。
  • 禁止 Store-Store 重排:如果汇编代码是 写操作 A --> 写操作 B,那么,CPU在执行时,绝不可能先执行 B 后执行 A。

综上,写操作前面的读写操作,都不能跑到该写操作的后面,天然满足 release 语义。

特别注意:release 语义是由 x86 体系结构(硬件)保证的,不是由 mov 指令保证的!mov 指令只能保证原子性,但该指令本身不禁止任何重排。

两者等价吗?

不等价

如上,两者翻译成相同的汇编指令,只能说是在 CPU 层面(硬件层面)等价。但在编译器层面(软件层面)不会等价。

从源代码到程序执行(进程/线程),可以简化成下面的模型(只为说明问题,所以只列出关键节点,不要扣细节上的对错):

源代码 --- 编译器进行汇编 ---> 汇编指令 --- CPU 执行 ---> 程序跑起来(进程 / 线程)

前面说过,CPU执行指令时,可能不会严格按照开发者看到的汇编代码的顺序执行。类似的,从源代码到汇编指令这一步,编译器也可能会重排。比如:

int data = 0;
std::atomic<int> flag{0};

// 情况 A:使用 release
void producer_release() {
    data = 42;                                // (1) 普通写
    flag.store(1, std::memory_order_release); // (2) 原子写
}

// 情况 B:使用 relaxed
void producer_relaxed() {
    data = 42;                                // (3) 普通写
    flag.store(1, std::memory_order_relaxed); // (4) 原子写
}

对于 producer_release():
编译器知道 (2) 是 release 语义,它必须保证data = 42flag = 1之前发生。生成的 x86 汇编大概是这样:

mov DWORD PTR data[rip], 42  ; 先写 data
mov DWORD PTR flag[rip], 1   ; 后写 flag (release)

对于 producer_relaxed():
因为是 relaxed,编译器发现dataflag是没有数据依赖的两个变量,为了优化寄存器分配或者流水线效率,编译器完全有可能把代码重排成这样

mov DWORD PTR flag[rip], 1   ; 编译器擅自把写 flag 提前了!(relaxed 允许这么做)
mov DWORD PTR data[rip], 42  ; 后写 data

所以,对开发者而言,如果想要 release 语义,就老老实实指定std::memory_order_release

load(relaxed)load(acquire)

store的情况类似,两者翻译成movq的原因是 TSO 内存模型也禁止 Load-Load 重排。因此:

TSO 内存模型禁止 Load-Load 和 Load-Store 重排 -->
    读指令后面的任何读写指令,都不可能排到该读指令前面 -->
        天然满足 acquire 语义

同样地,两者只是在 CPU 层面等价,但因为编译器重排的存在,两者整体上不等价。

store(seq_cst)load(seq_cst)

为什么两者翻译成的汇编代码不一样?

先说语义:

  • seq_cst作用在store上,具有 release 语义。(store本身就不可能有 acquire语义)
  • seq_cst作用在load上,具有 acquire 语义。
  • 除此之外,seq_cst还保证:程序中所有标记为**seq_cst**的原子操作都有一个全局唯一的执行顺序,并且所有线程看到的这个顺序都是相同的,即所谓的全局单一全序 (Single Total Order)。注意,这个约束只针对所有使用了seq_cst内存序的原子操作,而不是任意的原子操作。

前两点,x86 上已经在硬件层面天然保证了,关键看第三点。

A:  data_x.store(1, seq_cat)
  --> 假设翻译成 movq %edi, data_x(%rip)
    --> x86体系结构保证了,这个 movq 前面的读写指令都必须先于它被CPU执行(禁止 Load-Store / Store-Store重排)
        x86体系结构保证了,这个 movq 后面的写指令绝不能先于它被CPU执行(禁止 Store-Store 重排)
B:  data_y.load(seq_cst)
  --> 假设翻译成 movq data_y(%rip), %rax
    --> x86体系结构保证了,这个 movq 后面的读写指令绝不能先于它被CPU执行(禁止 Load-Load/ Load-Store重排)
        x86体系结构保证了,这个 movq 前面的读指令必须先于它被CPU执行(禁止 Load-Load 重排)

可见,光凭架构本身的约束,是不能禁止 B 跑到 A 前面(即 B 先于 A 被 CPU 执行 )的,因为 x86 本身不禁止 Store-Load 重排,这样,就违背第三点约束了。(Store-Load 重排的本质原因是 store buffer 的存在,详见前文对 Store-Load 重排的介绍)。

要保证第3点,除了强制 CPU 刷新 store buffer,避免 Store-Load 重排之外,还需要保证“不同原子操作的结果,被所有线程以相同的顺序看到”,比如原子操作 A 和 B,所有线程要么都先看到 A 的结果,后看到 B 的结果,要么反过来。不能有的线程先看到 A 的结果,有的线程先看到 B 的结果。比如典型的非多副本原子性 (Non-Multi-Copy Atomicity) 问题

非多副本原子性 (Non-Multi-Copy Atomicity) 问题

(即使全是读操作且不重排,不同观察者看到的世界也会不同)

这其实就是 IRIW(Independent Reads, Independent Writes)现象的根源。我们明确规定没有任何线程发生指令重排

初始:x = 0, y = 0

线程 1 (Core 1) 线程 2 (Core 2) 线程 3 (Core 3, 观察者) 线程 4 (Core 4, 观察者)
x = 1; y = 1; while(x == 0); while(y == 0);
r1 = y; r2 = x;

假设硬件拓扑结构是:Core 1 和 Core 3 物理距离近(比如共享 L2 缓存),Core 2 和 Core 4 物理距离近。

严格按序执行的过程:

  1. 线程 1 写入 x=1。这个消息很快传到了离它近的 Core 3,但还需要一段时间才能传到 Core 4。
  2. 线程 2 写入 y=1。这个消息很快传到了离它近的 Core 4,但还需要一段时间才能传到 Core 3。
  3. Core 3(线程 3)严格按序执行:它先看到了 x=1,跳出循环执行 ②。此时 y=1 的消息还没传到 Core 3,所以它读到 r1 = y = 0
  4. Core 4(线程 4)严格按序执行:它先看到了 y=1,跳出循环执行 ④。此时 x=1 的消息还没传到 Core 4,所以它读到 r2 = x = 0

结果: 同样出现了 r1 == 0r2 == 0

分析:
注意!线程 3 绝对没有把 ① 和 ② 重排,线程 4 也绝对没有重排 ③ 和 ④。
它们看到的分歧,纯粹是因为在多核系统中,数据在总线/缓存网络中的传播速度不一样(这在 ARM 和 PowerPC 等架构中是真实存在的)。

  • 线程 3 认为:x=1 发生在了 y=1 之前。
  • 线程 4 认为:y=1 发生在了 x=1 之前。
    它们又一次对“全局顺序”产生了分歧。

解决:

想要解决这个问题,系统必须保证,一个写操作要么对所有核心都不可见,一旦对某个核心可见,就必须瞬间对所有核心可见。不能出现“离得近的先看到,离得远的后看到”的情况。具体的,就是需要强迫缓存一致性协议(比如 MESI 等)进行昂贵的同步。

要解决上面的问题,就需要加 full memory barrier。一个显而易见的方法是在 A 和 B 上都加一个 full memory barrier,但这是冗余的,影响效率,因为只在某一个上面加就行了(详见前面对 full memory barrier 的解读)。

那么,加在谁上面呢?经过统计,多数程序中,都是读操作多于写操作,因此,加到写操作上,对效率的影响更小。所以,store(seq_cst)被翻译成了带隐式lock的 exchange 指令(xchglxchgq等),而load(seq_cst)依然被翻译成 mov 指令。

fetch_add

在 x86 上,fetch_add被统一翻译成lock xadd,即采用了最严格的内存序,加了 full memory barrier,为的就是保证“读-改-写”这一套流程的整体原子性,以及“结果对所有线程立即可见”(详见背景知识部分对 RMW 的介绍)。

所以,如果光从 CPU 的角度看,采用哪种内存序都是一样的。但是,如前所述,在编译器层面,不同的内存序依然是不一样的。

ARM64 体系下的store/load/ fetch_add

storeload

store(relaxed)load(relaxed)

两者不要求任何同步或者顺序约束,只要求原子性,因此翻译成普通的str((Store Register))和ldr((Load Register))指令即可。

store(release)/store(seq_cst)/load(acquire)/load(seq_cst)

为什么store(release)/store(seq_cst)都翻译成stlr(Store-Release Register),load(acquire)/load(seq_cst)都翻译成ldar(Load-Acquire Register)呢?

显然,两者分别满足 Release 语义和 Acquire 语义,所以用于store(release)load(acquire)是没问题的,那为什么可以用于store(seq_cst)load(seq_cst)呢?seq_cst要求的“全局单一全序 (Single Total Order)”如何来保证呢?

原因在于,ARMv8 的硬件内存模型设计得非常强,**stlr****ldar**指令的组合,不仅能满足 Acquire-Release 语义,在硬件层面本身就直接提供了 Sequential Consistency (SeqCst) 的保证

  • 硬件层面保证了:如果代码中先执行了一个stlr指令,随后又执行了一个ldar指令,CPU 硬件绝对不会把它们重排(No Store-Load Reordering)。详见官网文档
  • 另外,ARMv8 内存模型具备多副本原子性 (Multi-copy atomicity),即当一个stlr写入的值被某一个观察者(线程)看到时,它必然同时被所有观察者看到。详见官网文档

综合以上两点,ARM 硬件在执行全是stlr和ldar的代码时,天然就形成了一个所有线程一致认同的 Single Total Order。换言之,此处 ARM 硬件提供了比C++ 标准更强的保证。

xchgq: exchange,q 表示 Quadword,即64位。

该命令自带隐式 lock 语义。当 xchg 的操作数之一是内存地址时,硬件会自动为它加上 lock 语义。注意,如果 xchg 只在两个寄存器之间进行,则没有 lock 语义,只是普通的寄存器操作。

fetch_add

原子性的实现

data.fetch_add(1, std::memory_order_relaxed)为例,实际生成的汇编指令是下面这样:

.L6:
    ldxr    x2, [x1]
    add     x3, x2, 1
    stxr    w4, x3, [x1]
    cbnz    w4, .L6

ARM 架构没有类似 x86 的lock xadd单条原子指令,而是使用 独占加载/存储(Load-Exclusive / Store-Exclusive,即 LL/SC 机制) 配合循环来实现"读-改-写"的原子操作。

  • ldxr x2, [x1]: Load Exclusive Register。从x1指向的地址(即data)加载 64 位数据到x2。Exclusive 表示标记该内存地址的“独占监视器”,为后续的写操作做准备。
  • add x3, x2, 1: 计算x2 + 1,结果存入x3(即准备写入的新值)。
  • stxr w4, x3, [x1]: Store Exclusive Register。尝试x3(新值)写入x1指向的地址。写入的结果(成功或失败)会存入w4(32位寄存器):成功写入为 0,失败为 1。如果在此期间其他线程修改了该地址,独占监视器会被清除,导致写入失败。
  • cbnz w4, .L6: Compare and Branch if Not Zero。如果w4不为 0(说明写入失败,发生了并发竞争),则跳回.L6重新执行“加载-修改-写入”的循环。

内存序的实现

不难看出,内存序的实现,是通过替换 load / store 指令来试下的。例如,要实现 release 语义,就将stxr指令换成stlxr(Store Release Exclusive Register);要实现 acquire 语义,就将ldxr指令换成ldxar(Load Acquire Exclusive Register);要实现seq_cst语义,就同时替换两者。

与 x86 下fetch_add的区别

x86 下的fetch_add,所有内存序都与seq_cst等价,是最严格的,会产生 full memory barrier。

而 ARM 下,实现的是对应的内存序,不存在“越级”。