乒乓缓存与伪共享
这两个东西都是并行程序设计中必须考虑的问题
他跟多核 CPU 的缓存机制有关
CPU 缓存
因为 CPU 处理的速度远大于内存读写,因此在 CPU 的每个核心都会有小容量的高速缓存用于保存内存中的数据,CPU 的处理都会直接基于缓存而不是内存
把内存的东西搬到缓存里开销是比较大的,因此每次缓存的时候会把一块比较大的连续内存区域搬进去,这块区域就是一条 Cache Line
整块内存就会被划分为一条一条的 Cache Line
所以一个核心如果要访问某块内存,他必须要把这个块内存所在的 Cache Line 搬进自己的缓存里
乒乓缓存
如果两个在跑在不同核心上的线程同时访问同一块内存,比如访问同一个全局的原子变量
那么这两个核心的缓存中就都会有包含该变量的 Cache Line
然后一个线程使用原子操作修改了这个原子变量,那么另一个线程所在的核心就应该修改这个变量所在的 Cache Line,来让这两个核心上的这条 Cache Line 保持一致
于是另一个线程不得不等待缓存更新后才能继续处理
如果这样的线程很多,就会导致这条 Cache Line 在多个 CPU 核心之间传来传去,这就是乒乓缓存
这样的话,多个线程对同一个原子变量的原子操作,由于需要等待 Cache Line 反复传递,就会非常缓慢,即使这些线程是并行的
互斥锁也同理
但是有一个区别,互斥锁一般是操作系统级别,而原子操作一般是处理器级别
那么在等待缓存更新时,使用互斥锁的情况下,操作系统可以安排别的线程先执行;而如果使用原子操作,处理器只能继续当前的线程,也就是只能等待缓存的更新
另外,除了原子操作和互斥锁,java 中的 volatile 变量以及 C++ 中的内存屏障也都可能引发乒乓缓存
伪共享
跟乒乓缓存相类似,如果两个核心访问的不是同一原子变量,但是这两个原子变量在同一个 Cache Line 里,那么一旦产生修改这条 Cache Line 还是会被传来传去
举个简单的例子,一个处理器的四个核心各跑一个线程,分别处理一个长度为四的数组的四个原子变量(假设它们都在同一个 Cache Line 中)
看起来这四个核心互不影响,但一旦出现写操作,Cache Line 就还是会传来传去