一、多核处理器下的并发操作
问题
在多核处理器中,多个核心同时运行多线程程序。可能会发生以下情况:
- 核心 A 正在修改某个共享内存地址 X
- 核心 B 也想同时读或写 X
如果没有保护措施,就会出现数据竞争,即 B 读到 A 未完成的写,或者 A、B 的写操作交错导致数据不一致。
因此,硬件必须保证 原子性 ——即某些操作在多个核心之间看起来是“一次性完成”的。
解决方法:
-
锁总线:
当一个核心要执行原子操作的时候,CPU锁住整个内存总线,防止其他的核心访问内存。
- 缺点:效率低,整个总线被锁,其他线程甚至无法访问和当前内存无关的内存。
-
缓存一致性
原子性由缓存一致性协议 + 缓存行锁定来实现。
- 缺点:仍然可能退化成锁总线:
- 目标内存不在缓存对齐的 cache line 中(跨 cache line)
- 访问不可缓存内存(比如设备 I/O 映射区域)
- 缺点:仍然可能退化成锁总线:
二、缓存行
- CPU 缓存和内存之间传输的最小单位,主流大小是 64B。
2.1 结构
+----------------------+------------------+-------------------------+
| Tag (地址标签) | State bits (状态) | Data Block (数据) |
+----------------------+------------------+-------------------------+
1.MESI状态介绍
-
M (Modified)
- 本核心缓存里有最新的数据,主存是过期的。
- 必须在数据被替换(evict)前写回主存。
-
E (Exclusive)
- 本核心独占该缓存行,和主存一致,其他核心没有这个行。
- 可以直接修改而不需要通知别人(会升级到 M)。
-
S (Shared)
- 多个核心都有这块数据,内容和主存一致。
- 如果要修改,必须先通知别人使其失效。
-
I (Invalid)
- 缓存行无效,不能使用。
2.2 缓存对齐
-
缓存行对齐 :让数据结构(比如变量、数组、结构体)的起始地址,恰好落在 64 字节的倍数上,这样它能完整地装进一个缓存行,不会跨两个缓存行。避免读取的时候跨行操作,访问两次内存。
-
优点:
-
多线程下:可以避免伪共享
struct alignas(64) MyStruct { int x; double y; };会发现我的
MyStruct的大小应该是小于64的,那么会出现:MyStruct arr[10], 可能arr[0]-arr[2]位于同一个缓存行,那么当我多个线程分别操作arr[0]-arr[2]的时候,本来是可以独立运行的,但是由于位于同一个缓存行导致冲突。//同结构体内部也会发生这种事情 struct alignas(64) Data { int a; char pad[60]; // 填充到64字节 int b alignas(64); };
-