榆林市网站建设_网站建设公司_RESTful_seo优化
2025/12/18 3:33:42 网站建设 项目流程

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、CPU是如何获取数据的

cpu缓存

缓存行

CPU 获取数据的精简流程(按 “快→慢” 层级查,无效则找其他核心 / 主存):

  1. 先查自己核心的 L1 缓存:

    • 若 L1 里有该数据对应的有效缓存行,直接读;
    • 若 L1 缓存行无效,去查自己核心的 L2 缓存。
  2. 再查自己核心的 L2 缓存:

    • 若 L2 有有效缓存行,读并同步到 L1;
    • 若 L2 缓存行无效,去查所有核心共享的 L3 缓存。
  3. 接着查共享的 L3 缓存:

    • 若 L3 有有效缓存行,读并同步到 L2、L1;
    • 若 L3 缓存行无效,先看其他核心有没有该数据的有效缓存行:有则直接从其他核心读,同步到 L3、L2、L1;没有则从主存读。
  4. 最后从主存读:从主存加载该数据对应的缓存行,依次存入 L3、L2、L1,然后读取。

问:线程间的共享变量如何避免同时操作,操作的最新值如何实时同步到别的线程?下面是解答

二、原子性:

一个或一组操作,在执行过程中不可被线程调度器中断,且对外呈现 “要么完全执行完毕,要么完全不执行” 的状态 —— 不存在 “执行到一半” 的中间态,其他线程也无法观察到中间结果。

简单点讲:在该变量被操作的过程中,其他线程不可访问该变量(会阻塞到操作完成才访问成功),且该操作无法被中断。

关键特征:

  1. 不可中断:操作一旦开始,必须执行到结束,不会被其他线程打断(底层依赖 CPU 原子指令、锁、内存屏障等保证);
  2. 无中间态:从其他线程的视角看,操作要么 “没开始”,要么 “已完成”,看不到 “执行到一半” 的中间值;
  3. 单线程无意义:原子性是针对多线程的特性 —— 单线程下所有操作天然不会被中断,因此讨论原子性的前提是多线程竞争

三、原子操作

具备原子性的 “具体操作”:针对原子变量的操作

原子操作需要考虑内存序的问题:

内存序细讲就是控制可见性和顺序性,我们要基于变量对可见性和顺序性的要求选择合适的内存序

(一) 可见性

1. 定义

一个线程对原子变量的修改,其他线程能否及时、确定地读取到这个最新值

核心痛点:即使是原子操作,修改后的值可能只留在当前线程核心的私有缓存中,其他线程读的是自己缓存里的旧值(“修改了但看不见”)。

2. 原子操作可见性的问题根源

和普通操作一致(CPU 缓存 + 重排序),但原子操作可通过内存序解决:

  • 缓存层面:修改先写入核心私有缓存,未即时刷到缓存(可能仍存在寄存器中)、主存(如果有MESI机制的化,未刷新到主存不影响,会直接从缓存行同步到其他CPU缓存行);
  • 编译器 / CPU 优化:重排序或寄存器驻留,导致读取不到最新值。

3. 内存序如何解决可见性?

通过release(写)+acquire(读)组合强制同步缓存:

  • release:写原子变量时,强制将当前线程缓存中的所有修改刷到主存;
  • acquire:读原子变量时,强制从主存加载最新值(而非本地缓存)。

(二)顺序性

1. 定义

多线程下,原子操作的执行顺序是否和代码书写顺序一致,以及 “不同线程看到的操作顺序是否一致”。

核心痛点:编译器 / CPU 为了性能会重排序指令(单线程语义不变,但多线程下打乱顺序,导致逻辑错误)。

2. 原子操作顺序性的问题根源

指令重排序:比如反例代码中 “先写 shared_data,再写 data_ready”,CPU 可能重排为 “先写 data_ready,再写 shared_data”—— 线程 2 读到 data_ready=1 时,shared_data还没被修改(“顺序乱了”)。

3. 内存序如何解决顺序性?

内存序会约束 “哪些操作不能重排序”:

  • release:当前线程中,所有在 release 操作之前的写操作,不能被重排到 release 之后;
  • acquire:当前线程中,所有在 acquire 操作之后的读操作,不能被重排到 acquire 之前;
  • seq_cst:最严格,所有线程看到的操作顺序 “全局一致”(像单线程执行)。

反例:注意不一定报错(编译器不一定有重排,这里只是举个例子)

#include <atomic> #include <thread> #include <cassert> #include <iostream> // 全局变量: std::atomic<bool> data_ready(false); // 原子flag:标记data是否准备好 int shared_data = 0; // 普通变量:生产者要写入的数据 // 生产者:先写数据,再置位flag(代码书写顺序正确) void producer() { shared_data = 42; // 步骤1:写数据(代码顺序在前) // 错误:用宽松内存序,不约束操作顺序 data_ready.store(true, std::memory_order_relaxed); // 步骤2:置位flag(代码顺序在后) } // 消费者:看到flag置位,就读取数据 void consumer() { // 循环等待flag置位 while (!data_ready.load(std::memory_order_relaxed)) {} // 预期shared_data=42,但实际可能=0(因为重排序) assert(shared_data == 42 && "shared_data未正确初始化!"); std::cout << "消费者读取到shared_data = " << shared_data << std::endl; } int main() { // 启动生产者和消费者线程 std::thread t_prod(producer); std::thread t_cons(consumer); t_prod.join(); t_cons.join(); return 0; }

(三)内存序

内存序是 C++ 给原子操作提供的约束规则,本质是告诉编译器 / CPU:

  1. 哪些原子操作之间不能重排序
  2. 哪些原子操作的修改必须同步到其他线程(刷缓存)
内存序类型可见性保证顺序性保证适用场景
memory_order_relaxed无(仅保证原子性)无(操作可任意重排序)无依赖的计数器(如统计访问量)
memory_order_release修改对后续 acquire 读可见之前的写操作不能重排到该操作之后发布数据(写状态 / 指针)
memory_order_acquire能看到之前 release 写的所有修改之后的读操作不能重排到该操作之前获取数据(读状态 / 指针)
memory_order_acq_rel兼具 acquire(读)+ release(写)的可见性兼具 acquire+release 的顺序性原子交换、CAS 等读写操作
memory_order_seq_cst强可见性(全局同步)全局顺序一致(所有线程看到相同操作顺序)强同步场景(性能开销大)

总结:

概念核心目标依赖关系
可见性保证线程间修改能互相看到需内存序(release/acquire)约束
顺序性保证操作执行顺序符合代码逻辑需内存序约束重排序
内存序控制可见性 + 顺序性的规则不影响原子性,是解决前两者的手段

疑问:

问1:普通变量a++具备原子性吗?

以常见的编程语言(Java/C++/Python 等)为例,看似简单的a++本质是三步拆解操作,而非单一不可中断的指令:

  1. 读取(Load):将变量a的值从内存加载到 CPU 寄存器;
  2. 递增(Increment):在寄存器中对a的值执行+1运算;
  3. 存储(Store):将递增后的值从寄存器写回缓存(或内存)。

这三步是分步执行的,中间可能被线程调度器中断,导致多线程下的错误结果。

时间片线程 1线程 2内存中 a 的值
T1读取 a=0 到寄存器0
T2被线程调度器中断读取 a=0 到寄存器0
T3寄存器中 a+1=10
T4写回缓存(或内存),a=11
T5恢复执行,寄存器 a+1=11
T6写回缓存(或内存),a=11

最终a=1(而非预期的2),证明a++不是原子操作。

四、缓存一致性

缓存一致性(Cache Coherence)是硬件层面的核心机制—— 它解决了「多核心 CPU 的私有缓存中,同一变量的副本数据不一致」的问题,是原子操作能实现 “跨线程可见性” 的底层基础。

注意:缓存一致性应用于所有变量,但是不保证操作的原子性、顺序性。

(一)实现:

几乎所有现代 CPU 都用MESI 协议(Modified/Exclusive/Shared/Invalid)实现缓存一致性 —— 它给每个缓存行(缓存的最小存储单位)标记 4 种状态,通过总线广播状态变化,强制各核心同步缓存:

缓存行状态含义
M(Modified)缓存行被当前核心修改过,和主存数据不一致;仅当前核心持有该缓存行(独占)
E(Exclusive)缓存行和主存数据一致;仅当前核心持有(无其他核心共享)
S(Shared)缓存行和主存数据一致;多个核心持有(共享状态)
I(Invalid)缓存行无效(数据过期),必须从主存 / 其他核心重新加载

(二)MESI 协议如何保证缓存一致(结合原子操作举例):

假设初始时a=0,核心 1 执行原子自增,核心 2 执行原子读取:

  1. 核心 1 要修改a,先通过总线广播「请求独占a的缓存行」;
  2. 其他核心(如核心 2)收到广播,将自己缓存中a的缓存行标记为I(无效);
  3. 核心 1 将a的缓存行标记为E(独占),然后修改为 1,状态变为M(已修改);
  4. 核心 2 执行a.load()时,发现自己的缓存行是I,于是向总线请求a的最新值(数据来源优先级:M > E = S);
  5. 核心 1 收到请求,将a=1刷回主存并直接传给核心 2,自己的缓存行状态变为S(共享);
  6. 核心 2 从主存 / 核心 1 加载a=1,缓存行标记为S,读取到最新值。

整个过程中,MESI 协议通过 “状态标记 + 总线广播”,保证了核心 2 能拿到核心 1 修改后的最新值 —— 这就是缓存一致性的核心作用。

(三)缓存一致性救不了普通a++的核心原因:

缓存一致性不保证原子性,也就是核心1在执行操作的时候(还未更改完毕),核心2请求核心1的值,核心1很可能返回的是a的旧值1。

而原子变量,在操作前会发起lock指令,阻塞其他核心请求该值,直至操作结束完毕。

注意:

单靠原子性也不一定能获取到最新值:因为核心1操作完之后,最新值可能仍存放在寄存器中,并没有更新到缓存中,而此时lock释放,核心2获取的是旧值。

所以原子操作实时更新应该使用realse内存序,确保更新到缓存和主存。然后用acquire从主存中直接获取更稳妥!!!

疑问:

问1:MESI 中, 核心1持有a变量,此时核心2也要获取a变量,那是从内存中直接获取还是核心a同步给核心2?

在 MESI 协议中,核心 2 获取a变量时,优先从核心 1 的缓存中直接同步(缓存到缓存传输),而不是从内存获取—— 这是为了避免内存访问的高延迟,是现代 CPU 缓存一致性的高效实现方式。具体行为取决于核心 1 持有a对应的缓存行的状态:

分 3 种状态拆解(核心 1 的缓存行状态不同,同步方式不同)

假设核心 1 已持有a的缓存行,核心 2 发起 “获取a” 的请求:

1. 核心 1 的缓存行是M 状态(已修改 / 脏块)
  • 核心 1 的缓存行数据和内存不一致(比如核心 1 修改了a但没刷回内存);
  • 此时核心 2 请求时,核心 1 会:
    1. 先将缓存行的最新数据(比如a=1直接传给核心 2(缓存到缓存传输),同时把数据刷回主存(保证内存和缓存一致);
    2. 核心 1 的缓存行状态从M变为S(共享);
    3. 核心 2 接收数据后,缓存行状态设为S
  • 结论:核心 2 从核心 1 同步最新数据,而非从内存获取(内存里还是旧值)。
2. 核心 1 的缓存行是E 状态(独占 / 未修改)
  • 核心 1 的缓存行数据和内存一致,且只有核心 1 持有;
  • 核心 2 请求时,核心 1 会:
    1. 直接将缓存行数据传给核心 2(无需刷内存,因为数据和内存一致);
    2. 核心 1 的缓存行状态从E变为S,核心 2 的缓存行状态设为S
  • 结论:核心 2 从核心 1 同步数据(比从内存读更快)。
3. 核心 1 的缓存行是S 状态(共享 / 未修改)
  • 核心 1 的缓存行数据和内存一致,且可能有多个核心持有;
  • 核心 2 请求时,核心 1 会:
    1. 直接将缓存行数据传给核心 2(或核心 2 也可以从内存读,但缓存传输更快);
    2. 核心 2 的缓存行状态设为S
  • 结论:优先从核心 1 同步数据,少数情况(比如核心 1 的缓存行已被替换)才从内存获取。

缓存到缓存传输是 MESI 的高效优化

现代 CPU 都支持 “缓存到缓存传输”(Cache-to-Cache Transfer)—— 核心间可以直接通过总线传递缓存行数据,无需绕路主存。这是因为内存访问延迟(约 100ns+)远高于缓存间传输延迟(约 10ns 内),优先核心间同步能大幅提升性能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询