MESI 缓存一致性协议引发的一些思考

某个下午偶尔间看到了 MESI 缓存一致性协议,引出了我不少相关的疑惑,写下此文记录

通过这篇文章你能了解到的知识

  1. MESI 协议是什么,解决了什么问题
  2. 指令重排 是什么,解决了什么问题
  3. 内存屏障 是什么,解决了什么
  4. MESI 与 并发同步的区别

MESI 缓存一致性协议

MESI 产生的前提

  • 多级缓存的出现
  • 多核 CPU 的出现

总之为了计算机的性能,现代计算机都具备上述两点的设计,下图为不同存储的 IO 速度对比

MESI 缓存一致性协议引发的一些思考_第1张图片

MESI 解决的问题

由于现在计算机都是多核 CPU 了,并且每一个 CPU 都有自己独立的缓存(如下图架构),这样就会有可能多个 cpu 操作同一份数据,导致各个 cpu 缓存中的同一份数据值不一致的情况。

MESI 缓存一致性协议引发的一些思考_第2张图片

MESI 的具体设计

MESI:由缓存行(缓存中操作的基本单位,类似于磁盘的页)的四个状态首字母组成:

  1. Modified(修改):这一行数据修改过了,与内存中的数据不一致了,这意味着如果其它 cpu 中的缓存具有这行数据,其状态要修改成 invalid
  2. Exclusive(独占):只有一个 cache 读了这个数据行,并且没有修改过
  3. Shared(共享):有多个 cpu 的cahe 读了这个数据行,没有被修改过
  4. Invalid(无效):该缓存这一行的数据无效

状态流转

缓存行的状态流转如下图,通过对各个缓存行状态位的控制,达到了多核 cpu 缓存中数据一致的目的

MESI 缓存一致性协议引发的一些思考_第3张图片

一个例子

  1. 现在有两个 cpu,假定其缓存都为空,内存中有一个 x = 0 的数据
  2. cpu1 通过 bus 从内存中读取 x, 该缓存行状态设置为 E
  3. cpu2 通过 bus 从内存中读取 x,cpu 1 嗅探到地址冲突,两个 cpu 中 x 所在缓存行状态位设置为 S
  4. cpu1 需要修改其缓存中 x 的值,设置其 x 所在缓存行状态为 M,通知 cpu2 把 x 所在缓存行状态位设置为 I,再修改缓存中 x 的值

模拟网站:https://www.scss.tcd.ie/Jerem... (稍微有一些差异)

带来的问题

多个 cpu 的缓存状态置换是需要消耗时间,当一个 cpu 中缓存切换状态时,这个 CPU 需要等待其他 CPU 收到消息完成各自缓存中相应数据的状态切换并且发出回应消息。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

实际上,cpu 完全可以利用缓存行状态切换等待的这段时间去执行下一个指令,这就引出下文的指令重排

指令重排与内存屏障

上文中提到 cpu 可以不等待当前指令结果,直接去执行下一条指令,这其实就被称之为指令重排。

当然指令重排分为两个时期:编译时期,运行期。指令重排的目起都是为了追求性能,在以不改变原语义结果前提下乱序执行。这里的指令重排显然属于运行期,这里不讨论编译器发生的指令重排。

指令重排的实现

  • 存储缓存(store buffer):

    • 之前需要同步等待其它 cpu 返回的消息确认,然后修改缓存中的值
    • 现在直接把当前指令修改的结果放在存储缓存中,然后直接去执行下一次指令,等到异步收到其它 cpu 的确认消息后,再把存储缓存中要修改的值同步到缓存中
  • 失效队列(invalidate queue):

    • 之前当 cpu 检测到其它 cpu 发出的失效通知时,需要当前 cpu 停止手上的工作,完成对应缓存行的状态切换,回复确认消息
    • 现在直接把失效通知放在失效队列中,立马返回确认消息,以后再慢慢处理失效队列里的消息

MESI 缓存一致性协议引发的一些思考_第4张图片

指令重排带来的问题

并行环境下可见性问题

上述指令重排带来了并行环境下的可见性问题,因为上述的实现导致了处理器对数据的修改不是立即对其他内核可见的(store buffer 与 invalidate queue 都是异步处理的),这样在并发运行的程序下有可能会有数据不一致的产生。

内存屏障

cpu 并不知道什么指令能够重排序,什么指令不能够重排序,但 cpu 把这个任务交给了软件(程序员),这就是内存屏障。

内存屏障又分为四种:LoadLoad Barriers(读屏障),StoreStore Barriers(写屏障),LoadStore Barriers,StoreLoad Barriers

不同处理器对内存屏障的实现是不一样的,这里我们来分析下 x86 架构

读屏障

作用:所有读屏障之前发生的内存更新,对读屏障之后的 load 操作都是可见的

cpu 实际操作:失效队列(invalidate queue)里的实效指令(I)全部执行

写屏障

作用:所有写屏障之前发生的内存更新(M)对之后的命令都是可见的

cpu 实际操作:等到存储缓存(store buffer)为空(所有更新已刷出),cpu 才能执行写屏障之后指令

Full 屏障

作用:上述二者之和

cpu 实际操作:上述二者之后

总结

多核多级缓存计算机:提高单核无缓存计算机的性能,但会有缓存一致性问题

MESI:解决多核计算机缓存一致性问题,引发性能慢的问题

指令重排:缓解 MESI 性能问题,引出可见性问题(其实还是缓存不一致,无法保证实时性)

内存屏障:把并发存在的可见性问题交给软件(程序员)去解决,软件来告诉 cpu 哪些指令不能重排序

总之这一系列下来是为了提高了计算机性能和保证了缓存数据一致性

思考

MESI 缓存一致性 与 并发同步的区别?

一个是作用于 CPU(缓存),一个作用于线程。一个 cpu 可以对应多个线程。实际上并发同步考虑的是线程与内存交互,并看不到中间还有缓存。

我们先设想在单核机器上

  • 单核机器不存在缓存一致性问题,但还会存在多线程并发同步的问题

假如在多核机器上

  1. 假设有 a,b 两个线程分别运行在 cpu1,cpu2 上,内存中有个变量 x,两个线程它们操作是对 x++ 。
  2. 并发控制需要对 x++ 这一操作加锁,意思就是两个线程不能同时对 x 进行操作,两个线程运行 x++ 时间必须是串行的,即使它们分别在两个 cpu 上
  3. 在并发控制中看到的是:a 线程对 x++,把 x=1 写入内存,然后 b 线程读取内存中 x 的值, x++,把 x=2 写入内存
  4. 然而在 cpu 看来,它是直接与缓存打交道的。如果没有 MESI 缓存一致性协议,它看到有可能的是:a 线程从 cpu1 缓存中读到 x=0, x++ ,把 x=1 写入缓存,然后 b 线程从 cpu2 的缓存中读到 x = 0, 把 x = 1 写入缓存。
  5. 但实际上,并发控制是依赖 MESI 缓存一致性协议的。a 线程从 cpu1 的缓存中读到 x=0,x++,把 x=1 写入缓存,同时使 cpu2 上的 x=0 的缓存失效并且写回到内存。然后由于线程 2 中 x 的缓存是失效的,只能从内存中读取 x=1,x ++,把 x=2 写入内存 同时使 cpu1 上 x=1 的缓存失效。

总结来说并发控制保证多线程在临界区上串行执行,而在多核计算机中,又依赖缓存一致性保证结果的正确性。

参考资料

  1. https://zhuanlan.zhihu.com/p/...
  2. https://www.cnblogs.com/hello...
  3. https://stackoverflow.com/que...
  4. https://www.zhihu.com/questio...

你可能感兴趣的