操作系统的内存模型

所有的数据运算都需要通过CPU来执行,所有的数据都是存储在内存中的,但是CPU的速度非常快,而内存的速度相比于CPU来说非常慢,因此CPU不能直接去操作内存,所以CPU设置了一系列的高速缓存,就是我们常常看到的一级缓存(L1)、二级缓存(L2)、三级缓存(L3)等,如果需要CPU需要操作数据,则首先将数据从内存(主内存)中读取到高速缓存中来,然后再层层传递直到传递到寄存器,CPU就可以通过指令集对数据进行运算了

image.png

如果你的系统只有一个CPU,那么就不会有数据的线程安全问题,因为同时只会有一个线程来操作数据,再将数据写回主内存,但是现代计算机基本不会再有单CPU的机器了,所以要面对多线程的情况,比如两个线程同时对变量i进行++操作,按理来说结果应该是i = 3才对,但是由于高速缓存的存在,导致出现下面的结果

image.png

变量i经过两次操作之后的结果还是2,很明显,在CPU1对变量i进行修改的时候从内存中读取到的i值为1,然后缓存到自己的高速缓存中并执行i然后将结果再次缓存到高速缓存再写入到主内存,CPU2在同时也做了同样的操作,所以导致了最后i++两次之后的结果错误,这种错误不是我们想看到的

CPU为了解决这种问题,有两个解决方案

  • 总线锁:在CPU操作数据时会锁定数据锁在的主内存块,在操作期间其他CPU都无法操作该块内存的数据,因此也就不会出现数据错误问题,但是这种总线锁效率比较低,所以CPU又有了一种新的解决方法,缓存锁

  • 缓存锁:缓存一致性协议(MESI),针对于上面这种情况,如果CPU将变量i的值写入到了主内存,那么它会通知其他CPU,该数据已经被修改过了,其他CPU的数据都已经失效了,需要重新从主内存中读取该数据

    • MESI:其实对应了缓存的四种状态:
      • M,Modified,缓存有效,但是和内存中的数据不一致了
      • E,Exclusive,互斥,缓存有效,内存和缓存的数据一致并且数据只存在于当前缓存中
      • S,Shared,共享态,缓存有效,内存和缓存数据一致并且数据存储在多个缓存中
      • I,Invalid,缓存无效

在上面的例子中。变量i就是共享变量,因此其在CPU中的缓存初始状态为S,接着CPU1对变量i进行写入,CPU1本地缓存状态转换路径为S -> E -> M,CPU2中变量i的缓存状态转换路径为 S -> I,也就是缓存失效,那么CPU2会再次从主内存中读取最新的变量i的值

并发的三大问题

  • 原子性
  • 有序性
  • 可见性

原子性

原子性,即一个操作是不可被分割的,例如i这个操作,其实可以被分为3步操作,首先读取变量i的值,然后对其值加1,再将新值写入到i所在的内存中,那么i就不是一个原子操作

有序性

有时候为了提高代码的执行效率,CPU和编译器可能会对指令进行重排序,在不改变代码运行结果的前提下改变代码的执行顺序

可见性

多线程工作时,对共享变量的修改操作,比如CPU1修改了变量i的值然后写入主内存,在CPU1写入主内存之前CPU2就需要用到变量i了,于是CPU2从主内存中读取到了变量i的旧值,然后CPU1才将变量i的值写入到主内存,这就是可见性问题,多CPU环境下对共享变量的修改不能及时被其他CPU看到