多线程-缓存行

57 阅读2分钟

一、多核处理器下的并发操作

问题

在多核处理器中,多个核心同时运行多线程程序。可能会发生以下情况:

  • 核心 A 正在修改某个共享内存地址 X
  • 核心 B 也想同时读或写 X

如果没有保护措施,就会出现数据竞争,即 B 读到 A 未完成的写,或者 A、B 的写操作交错导致数据不一致。

因此,硬件必须保证 原子性 ——即某些操作在多个核心之间看起来是“一次性完成”的。

解决方法

  1. 锁总线:

    当一个核心要执行原子操作的时候,CPU锁住整个内存总线,防止其他的核心访问内存。

    • 缺点:效率低,整个总线被锁,其他线程甚至无法访问和当前内存无关的内存。
  2. 缓存一致性

    原子性由缓存一致性协议 + 缓存行锁定来实现。

    • 缺点:仍然可能退化成锁总线:
      • 目标内存不在缓存对齐的 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);
      };