1. CPU高速缓存
由于CPU运算速度要比内存读写速度快的多,所以如果CPU每次执行指令都直接和内存交互的话,则会花费大量的时间等待数据读取和数据写入,在等待的过程中我们的CPU资源则处于“浪费”状态, 于是乎,CPU高速缓存出现了。
高速缓存位于CPU寄存器组和内存之间,它的速度远高于内存但是也比不上CPU内部的寄存器组。
按照数据读取顺序和与CPU结合的紧密程度,CPU缓存还可以进一步分为一级缓存、二级缓存、三级缓存等。
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有找到就从三级缓存或内存中查找。
有了多级缓存之后,CPU对于数据的读取和存储都会经过高速缓存,CPU与高速缓存有一条特殊的快速通道相连,而主存与高速缓存都连在系统总线上
1.1 运行原理
CPU执行指令的时候如果涉及到数据的读写操作,会将运算需要的数据从主存(内存)复制一份到高速缓存当中,接下来CPU计算时就可以直接在它的高速缓存上进行数据的读写,当运算结束之后,再将数据从高速缓存刷新到主存当中,比如:i = i + 1;
当CPU执行这个语句时,会先从主存(内存)中读取 i 当前的值,然后将其复制一份到高速缓存,这时相当于高速缓存中有了 i 的一个副本,接下来对该副本进行加 1 操作,加法完成后将结果写入高速缓存,最后再将高速缓存中的值刷新到主存
单核单CPU时代,所谓的多线程都不是真正意义上的多线程,因为一个CPU内核在同一个时间点只能执行一个任务,这个时候的多线程是通过分配CPU时间片实现的,执行一个任务的时候,其它任务则等待,只是等待的时间很短,这让我们看起来像是多个线程在同时运行,现在多核CPU已经成为常态
2. 存在的问题
i = i + 1 在多线程环境下,设 i 的初始为 1,如果我们有A、B两个线程同时执行这行代码,那么每个线程都会发生上述的计算流程。这个时候可能会出现一个情况(当然还有其它情况会导致类似的结果):
- A线程和B线程都从主存中读取了i的值,并且缓存到各自的高速缓存中,此时两个副本都为1;
- 然后A线程进行加1操作,副本值变成了2,最后回写到主存中,主存中的值为2;
- 此时B线程的副本值还是1,加1操作的结果还是为2,然后将其回写到主存
- 最终主存的值为:2
但是我们的预期应该是3才对,因为毕竟执行了两次加1操作嘛。这个就是我们通常说的缓存一致性问题了,在主存中的值可以被所有线程访问,所以我们称之为共享变量,高速缓存中的副本值只能自己访问,所以称之为共享变量的私有拷贝
3. 解决方案
缓存一致性的问题归根结底是由多个线程同时操作一个共享变量导致的,所以要解决这个问题其实很“简单”:只需要保证共享变量在被某一个线程操作的时候,其它线程都不能对其操作即可。对应到上面的例子也就是:如果A线程先处理共享变量 i 的值,B线程就只有等待A线程处理,并且A线程将最新的值更新到主存之后,B线程才能获取变量 i,再进行操作,这样就能保证结果不会有问题啦
主存和高速缓存连在系统总线(Bus)上,它们通过总线进行通信。所以可以通过在总线上加锁(LOCK)的方式达到目的。比如A和B要从主存中获取变量 i 的时候,先在总线上发起LOCK信号,如果LOCK成功才能获取变量 i 的值,否则只能等待LOCK被释放才能从其内存地址获取值。
在LOCK总线的期间,其它线程都只能等待,不能访问该内存区域,导致了效率低下,又特别是对共享变量根本就只有读操作,不存在写操作的时候,更加让人不能忍受,于是就出现了缓存一致性协议。最出名的也就是Intel 的MESI协议
3.1 MESI
MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
它的原理是: 当CPU写数据时,如果发现操作的变量是共享变量,即在其它的CPU中存在该变量的副本,会发出信号通知其它CPU将该变量的缓存行置为无效状态,然后当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,它就会从内存重新读取
- Modified(M):已修改。说明变量已经被持有它的处理器修改,如果一个段处于该状态,那么它在其他处理器缓存中的拷贝马上会变成“失效”(Invalid)状态。另外,已修改缓存段如果被丢弃或标记为失效,那么先要把它的内容回写到内存中。
- Exclusive(E):独占。只能被一个处理器持有该状态,一旦一个处理器持有了该状态的缓存段,那其它处理器就不能同时持有它。如果其他处理器本就持有了同一缓存段,那么它会马上变成“失效”(Invalid)状态。
- Shared(S):共享。这种状态下的缓存段只能被读取,不能被写入。
- Invalid(I):失效。如果缓存段为该状态,就相当于它从来没被加载到过缓存中一样
只有当缓存段处于Modified或Exclusive状态时,处理器才算真正“持有”它,才能对其发起写操作
如果它没有持有该缓存段,又想要对其发起写操作,就需要先向总线发出申请:我想要写数据啦,可以不?只有当申请通过后才能执行写操作,同时如果其它处理器存有该变量的私有拷贝, 那么总线会通知它们:这个东西已经被某某持有啦,你们的缓存段即将失效!即处于Invalid状态。这时,该共享变量仅仅在持有它的处理器中有效,所以就不会有问题了。另外如果其它处理器要读取该缓存段,那么Exclusive或Modified状态的缓存段必须变为Shared状态,如果是Modified状态,则还需要将更新后的数据写回主存
根据MESI协议,当某一个变量被某一个处理器独占时,其不会出现在其它处理器的有效缓存中,另外,它能保证数据如果被更新,然后被写回主存后,所有处理器的缓存段内容的有效值都和主存中的最新值对应