CPU的多级缓存

引言

这个是我起初学习并发编程时,记录的一些笔记。很多并发的教学都是从代码级别去告诉我们应该怎么做,否则会出现什么后果,但很少能够说明并发编程的起源与问题的根本。
所以,我觉得,并发编程要软硬结合着来理解和学习。

CPU多级缓存

由于CPU的频率太快了,导致主存跟不上,这样就导致了CPU要等待主存,造成了资源的浪费,所有CPU使用cache来调节CPU和主存速度差异太大的问题。

CPU的多级缓存就是我们常听说的CPU一级缓存、二级缓存、三级缓存这些缓存的总称,又叫做高速缓存。多级缓存是一个倒立的金字塔结构,越是底层,缓存的大小就越小,但是速度就越快。计算机中最快的存储内存就是寄存器,然后是高速缓存,然后是主内存,最后才是硬盘。

主存中的数据都是经过高速缓存才被CPU-Core处理的。因此,数据的读取和存储都经过高速缓存。主存和高速缓存都连在系统总线上,这条总线还用于其他组件的通信。

缓存的意义

缓存很小,因此无法存储CPU所需要的所有数据。那么缓存的何在呢?
1. 时间局部性 如果某个数据被访问,那么在不久的将来它很可能被再次访问。
2. 空间局部性 如果某个数据被访问,那么与它相邻的数据很快也可能被访问。

缓存一致性(MESI)

MESI是一种协议,它是用于保证多个CPU Cache之间缓存共享数据的一致。MESI为了保证一致性,定义了Cache Line的四种状态,而CPU对Cache的四种操作可能会产生不一致的状态。因此缓存控制器监听到本地操作和远程操作的时候,需要对地址一致的cache line状态做出一定的修改,从而保证数据在多个cache中的一致性。

缓存的读写事件可分为

  • 本地读写:即,读取当前缓存中的数据。
  • 远程读写:即,读取其它缓存中的数据(包括读取主存中的数据)。

举个例子:
1. 假设目前有三个CPU,分别是 a b c, 然后每个CPU都有它自己的缓存,分别是 a1 b1 c1,并且系统中还有个主存。
2. 假设现在有个变量 x 在主存中,然后 cpu a 要处理 x ,于是 变量 x 被读取到 a1 中,此时,x 被 cpu a 独享,所以 x 是 E(独享状态)。
3. 接着,其它 cpu 也要处理变量 x ,然后 b、c就从主存中读取 x,这个事件会被 cpu a 发现,于是 ,a1、b1、c1 中的 x 的状态 S(共享状态)。
4. 然后,假设a要对x的值做出了修改,那么会将 a1中的 x 状态修改为 M(修改状态),并通知 b、c 将 b1、c1 中的x修改为 I (invalid无效状态)。
5. 最后,如果其它cpu要处理x,并且发现x在本地缓存中是 无效状态,就会通知 a 将 x 的值写到主内存,并且 a 会将 x 设置为 E 状态。

MESI导致的问题

MESI解决了cpu 的缓存一致性的问题。但是同时,带来了新的问题,那就是缓存一致性的消息传递是需要时间的,这就使 变量 x 的状态切换会出现延迟。因为当 x 在 a1 中切换状态时, a会通知 b、c 将 它们的 x 也进行状态修改,最后 b、c对 x修改后,会反馈给 a。这个过程需要时间,在这段时间中 CPU会等待所有缓存响应完成。

比如你需要修改本地缓存中的一条信息,那么你必须将I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。因为这个等待远远比一个指令的执行时间长的多。

CPU切换状态阻塞解决-存储缓存(Store Bufferes)

失效确认(Invalidate Acknowledge)就是 b、c将 x 状态置为 Invalid 状态后,并发给 a 的通知。为了避免这种CPU运算能力的浪费,Store Bufferes被引入使用。

处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。但是这么做有两个风险。

Store Bufferes的风险

  1. 就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
  2. 保存什么时候会完成,这个并没有任何保证。就有可能导致 重排序(reordings)。

Volatile的作用

普通变量会按照上面的过程被处理,所以在多线程情况下,普通变量不加保护措施被多线程共享是很有可能会出现问题的。

但是, volatile 类型的变量不会有 可见性的问题,因为,volatile类型的变量始终是在主存中的,也就是说,CPU不会将 volatile类型的变量缓存到CPU缓存中,CPU每次处理volatile类型的变量时,都会从主存中读取,并且跳过cpu cache。

所以,volatile类型的变量就不存在 可见性的问题(因为每次都修改都是基于主内存,并不是基于cpu cache)。简而言之就是:Volatile修饰的变量在值被修改后会立即刷新到主内存。

但是,这样并不能保证 volatile 类型的变量是 线程安全 的,因为,并发不仅仅有 可见性问题,还有原子性问题。所以上面的机制无法保证 volatile 类型变量的原子性(因为还是有可能多个线程同时操作这一个变量),故,在多线程情况下 ,volatile类型的变量还要使用锁来保证原子性。

  • 在 Java 中 volatile、synchronized 和 final 实现可见性。
  • 在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
    其实还有个有序性啦,但是保证了原子性和避免了重排序就间接的保证了有序性。

Related Posts

发表评论

电子邮件地址不会被公开。 必填项已用*标注

© All Right Reserved