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

并发编程核心概念辨析

一、背景:CPU 多级缓存架构

为了读者在阅读后序章节时,有更清晰、更形象的认知,这里放上现代 CPU 缓存的典型结构。一图胜千言,不多赘述,仅陈述以下要点:

  • 一颗 CPU,往往有多个核心(core)。同一时刻,每个 core 上都可以运行一个线程(thread)。
  • 为了追求更高的效率:
    • 编译器 / CPU 可能重排某些指令,把后面的提前执行。
    • CPU 增加了多级缓存。

这使得某些情况下,某个线程看到的数据,可能是非预期的,进而导致程序出现逻辑错误。

当然,这并不是说编译器或者 CPU 的设计有缺陷,而是一种平衡与妥协:为了效率, 编译器 / CPU 会适度“放宽政策”,不做过于严苛的约束和检查,这在多数情况下是安全的;为了正确性,在并发编程时,开发者需要更加精细的干预。

🔍 点击查看图片

二、概念辨析

1. 缓存一致性(Cache Coherence)

层次:纯硬件机制,在 CPU 内部固化。

解决的问题同一个内存地址(单一变量)多个CPU 核心的本地缓存(L1/L2 Cache)中如何同步。即,一个核心的写入如何让其它核心感知?

核心0的L1缓存: [addr X] = 1 ← 我刚写入
核心1的L1缓存: [addr X] = 0 ← 这里还是旧值,怎么同步?

机制:MESI、MOESI、MESIF协议(状态机)。注意,这些缓存一致性协议是固化在CPU内部的,纯硬件实现。

M (Modified) - 该 cache line 仅存在当前 cache 中,与内存不一致(dirty),其他 CPU core 缓存无效
E (Exclusive) - 该 cache line 仅存在当前 cache 中,与内存一致(clean)
S (Shared) - 该 cache line 被多核共享,且与内存一致
I (Invalid) - 该 cache line 已失效,需重新加载

关键特性

  • 地址/变量
  • 最终一致,但不保证何时能看到(因为有 store buffer)。
  • 不保证多个地址之间的对外可见顺序(核心1可能先看到先看到修改后的变量A,后看到修改后的变量B;核心2可能正好赶过来)。
  • 对程序员完全透明,你不需要也不能直接操控它。

通俗理解:它是底层的“群消息同步机制”,保证群里所有人看到的单条消息内容是一致的。

2. 内存一致性模型(Memory Consistency Model)

层次:硬件架构规范层,由 CPU 架构决定,不同类型的 CPU 不一样。

解决的问题:多个地址上的读写操作,从其它核心观察时,顺序会不会乱?允许哪些重排?

这才是真正决定"多线程程序正确性"游戏规则的模型:

允许的重排
模型 Load-Load Load-Store Store-Store Store-Load
───────────────────────────────────────────────────────────────────────────────────────
Sequential Consistency (SC) × × × ×
Total Store Order (TSO) × × × ✓ ← 只允许这个
Relaxed / Weak Consistency ✓ ✓ ✓ ✓

不同架构的选择

  • x86/x64:TSO(Total Store Order)——接近最强,只允许 Store→Load 重排。
  • ARM:非常宽松(weakly ordered),几乎允许所有重排。
  • POWER:类似 ARM,甚至更宽松。

缓存一致性 vs 内存一致性模型的区别

  • 缓存一致性回答:一个地址的写入最终会传播吗?答:会(最终所有核心一定能看到一致结果)
  • 内存一致性模型回答:多个地址的操作,以什么顺序传播?是否允许后操作的先被看到?

通俗理解:你发了消息 A,又发了消息 B。内存一致性模型决定了,群里其他人有没有可能先看到 B,后看到 A。

3. 内存屏障(Memory Barrier / Fence)

层次:硬件指令 + 编译器指令

解决的问题:在代码的特定位置,强制约束重排序边界。是程序员/编译器用来干预“内存一致性”的物理武器。

类型

  • Load Barrier:屏障前的所有 load 操作,必须在屏障后的任何 load 操作开始之前,全局完成(对其他处理器可见)。
  • Store Barrier:屏障前的所有 store 操作,必须在屏障后的任何 store 操作开始之前,全局完成(从 Store Buffer 刷新到 L1 Cache,并对其他处理器可见)
  • Full Barrier: 屏障前的所有读和写操作,必须在屏障后的任何读和写操作开始之前,全局完成。

具体指令

x86:
LFENCE → Load Barrier
SFENCE → Store Barrier
MFENCE → Full Barrier(最常用)
LOCK前缀 → 隐含 Full Barrier
ARM:
DMB ISH → Full Barrier(数据内存屏障)
DSB ISH → 更强的同步屏障
ISB → 指令同步屏障(刷流水线)

两种屏障(注意区分):

// 编译器屏障(只防止编译器重排,CPU不受限)
asm volatile("" ::: "memory"); // GCC
_ReadWriteBarrier(); // MSVC
// 硬件屏障(同时防止编译器重排 + CPU重排)
asm volatile("mfence" ::: "memory"); // x86 Full Barrier

4. 内存序(Memory Order)

层次:C++ 编程语言层,C++11 引入。

本质:是对内存屏障的高级抽象,让程序员用语义而非汇编指令来表达需求。

机制:你写下memory_order,编译器会根据当前的 CPU 架构(x86 还是 ARM),自动帮你翻译成对应架构的内存屏障指令(Memory Barrier)。

六个级别

relaxed → 只保证原子性,不产生任何屏障
acquire → Load 时用:屏障后的操作不能重排到此 Load 之前
release → Store 时用:屏障前的操作不能重排到此 Store 之后
acq_rel → 用于 RMW:同时具备 acquire + release 语义
consume → acquire 的弱化版(实践中几乎不用)
seq_cst → 最强:全局顺序一致,等价于 Full Barrier

编译器如何翻译

C++ memory_order x86 生成 ARM 生成
─────────────────────────────────────────────────────
relaxed load → MOV LDR
relaxed store → MOV STR
acquire load → MOV LDAR
release store → MOV STLR
seq_cst store → MOV + MFENCE STLR + DMB
seq_cst load → MOV LDAR

x86 上 acquire/release 不需要额外指令(因为 TSO 已经提供了大部分保证),ARM 上需要专用指令。

5. 四者关系

🔍 点击查看图片

它们的依赖关系

  • 缓存一致性:硬件底座,没有它,写入根本无法传播,其他一切无从谈起。
  • 内存一致性模型:硬件架构契约与规则,定义了默认允许什么、禁止什么。
  • 内存屏障:硬件指令,是工具和手段:当默认规则不够用时,用它来强化约束。
  • 内存序:是软件层高级抽象,C++ 程序员通过它告诉编译器需要什么保证

三、内存序/内存屏障的作用范围

作用1:防止当前线程内的指令重排(编译器 + CPU)

// 没有屏障,编译器和CPU可能重排这两条指令
data = 42; // 可能被移到 flag store 之后!
flag = true;
// 有 release 屏障,data = 42 一定在 flag = true 之前完成
data = 42;
flag.store(true, memory_order_release); // 屏障

作用2:控制跨线程的可见性时序

通过控制“store 何时变得全局可见”和“load 何时看到最新值”来影响其他线程的观察结果

内存屏障的物理效果(以 x86 TSO 为例):
CPU Core 0: Store Buffer Cache(共享)
─────────────────────────────────────────────────────────────
store data = 42 → 进入 Store Buffer → [等待提交]
store flag = true → 进入 Store Buffer → [等待提交]
MFENCE → 强制刷新 Store Buffer → data=42, flag=true 提交到 Cache
CPU Core 1: Cache(共享)
─────────────────────────────────────────────────────────────
load flag ← 从 Cache 读(必须看到 flag=true 后才能继续)
MFENCE / acquire ← 确保后续 load 看到最新 Cache 状态
load data ← 从 Cache 读 → 一定是 42

关键点

  • 内存屏障强制刷新 Store Buffer,让 store 提交到 Cache(对其他核心可见)
  • 缓存一致性协议(MESI)负责在 Cache 之间传播这个更新
  • acquire load 确保从 Cache 读时看到最新状态(不使用过期的缓存行)

所以:

防重排(编译器/CPU内部)+ 强制可见性(跨线程)
↑ ↑
同一个机制,两种效果,不可分割

四、Store-Load 重排

1. 原因

根本原因:Store Buffer(写缓冲区)

当 CPU 执行写操作(Store)时,如果直接写入L1 Cache,由于多核之间的“缓存一致性协议(如MESI)”,CPU 必须等待其他核心确认并作废它们对应的缓存行,这个等待过程比较漫长(CPU 视角)。为了不阻塞 CPU,核心会先把数据写到 Store Buffer 中,然后继续执行后续指令。

现代 CPU 架构:
CPU Core
↓ store
[Store Buffer] ← store 先写这里(速度快,不用等Cache响应)
↓ 异步刷新
[L1 Cache]
[L2 Cache]
[LLC / 内存]
问题场景(Dekker 互斥算法的经典失败案例):
初始值:X = 0, Y = 0
Thread 1: Thread 2:
store X = 1 store Y = 1
load R1 = Y load R2 = X
期望:R1=1 或 R2=1 至少有一个成立
实际:R1=0 且 R2=0 竟然可能发生!(x86上也会!)

时序解析

Time →
Thread 1: store X=1 → [Store Buffer] ← 还没提交到 Cache!
Thread 1: load Y=0 ← 从 Cache 读(Y 的 store 还在 Thread2 的 Store Buffer 里)
Thread 2: store Y=1 → [Store Buffer] ← 还没提交到 Cache!
Thread 2: load X=0 ← 从 Cache 读(X 的 store 还在 Thread1 的 Store Buffer 里)
// 结果:R1=0, R2=0。两个 store 都"消失了"

Store-Load 重排的本质:不是 CPU 真的"调换了顺序",而是 store 在 store buffer 里异步等待,而 load 已经直接去 cache 读了。效果上等价于 load 跑到了 store 之前。

Store-Load 重排的理解:从外部观察者(其它 CPU core)的视角看,load 跑到 store 的前头了。因为从外部观察者的立场来看,store完成的标志,是“你得让我看见”。现在,在没有让我看到你写的值的情况下,你先执行了后面的 load 指令,那对我来说,你就是先读后写了。

注意:x86/TSO 只允许 Store→Load 重排,其他三种(Load-Load, Load-Store, Store-Store)x86 不允许。ARM 四种都允许。

2. 解决方案

方法1:在 store 和 load 之间插入 Full Barrier

// x86
asm volatile("mfence" ::: "memory");
// C++ 标准方式
std::atomic_thread_fence(std::memory_order_seq_cst);
Thread 1:
store X = 1
MFENCE ← 强制刷新 Store Buffer,X=1 提交到 Cache
load R1 = Y ← 此时 Y 的最新值一定可见
Thread 2:
store Y = 1
MFENCE
load R2 = X ← 此时 X=1 一定可见

方法2:使用 seq_cst 原子操作

std::atomic<int> X{0}, Y{0};
// Thread 1
X.store(1, std::memory_order_seq_cst); // 含隐式 Full Barrier
int r1 = Y.load(std::memory_order_seq_cst);
// Thread 2
Y.store(1, std::memory_order_seq_cst);
int r2 = X.load(std::memory_order_seq_cst);
// 保证:r1=1 或 r2=1 至少一个成立
http://www.jsqmd.com/news/1138041/

相关文章:

  • 免费前端开发库认证终极指南:从零到专业的三步进阶法
  • 131.IEC61131-3 标准编程!CODESYS/TwinCAT 通用分拣控制工程方案
  • 什么是软件?别再说“就是代码”了,那只是半成品
  • Java金融系统银行级安全加固实战指南:从代码到运维的纵深防御
  • STM32L081CB驱动WS2812 LED灯带的实战指南
  • 别被“函数”吓住——C语言函数,其实就是你的“万能小工具”
  • 如何使用Conda国内源安装虚拟环境(保姆级全平台教程)
  • 5步完成老Mac系统升级:OpenCore Legacy Patcher终极指南
  • Cascade R-CNN vs Iterative BBox vs Integral Loss:3种级联结构对比与性能差异解析
  • 小白程序员必看:收藏这份大模型应用开发转行指南,36岁也能吃香!
  • Paperxie 聊毕业论文 AI 写作|学长学姐实测,一步一步写学位论文真的省心
  • 天天说“鲁棒性“,到底啥意思?一篇给小白讲清楚
  • 136、DySample 动态上采样在 YOLOv11 中的实现:基于点采样的轻量级动态上采样器
  • 终极UltraStar Deluxe免费开源卡拉OK唱歌游戏完整指南:如何快速上手专业级K歌体验
  • Paperxie 毕业论文智能写作好用吗?过来人完整实测对话版
  • 美团 LongCat-2.0 正式 MIT 全开源|1.6T MoE 大模型技术拆解与完整本地部署实战教程
  • 终极跨语言阅读解决方案:DeepL翻译插件如何让外文内容秒变中文
  • 终极配置指南:vJoy虚拟摇杆完整教程
  • Service Mesh 超时重试:救服务,也可能放大故障
  • 抖音音频批量下载终极指南:免费开源工具快速获取无水印音乐
  • Java毕设选题推荐:基于 SpringBoot 的教学过程考核评价系统的设计与实现 基于 SpringBoot 的课程阶段性考核管理系统【附源码、mysql、文档、调试+代码讲解+全bao等】
  • AI产品的技术债务管理:如何在快速迭代中守住代码质量底线——不是消灭所有债务,而是控制债务的利率
  • 如何识别PARP14
  • 周一上线:被 Claude 封号算工伤吗?贾扬清据报离开 NVIDIA,Cursor 推出 iOS 版
  • 综合项目三:Web全栈应用从零部署
  • 0x03 RLPD 训练范式全解析
  • 告别网络限制:VBrowser-Android视频缓存解决方案
  • 如何轻松为任何游戏注入自定义插件:终极ASI加载器完整指南
  • Java毕设选题推荐:基于 SpringBoot 的家政订单预约管理系统的设计与实现 基于 SpringBoot+Vue 的家政人员资质审核系统【附源码、mysql、文档、调试+代码讲解+全bao等】
  • Kindle Comic Converter终极指南:将漫画完美适配电子墨水屏的免费工具