问题引入
在计算机执行程序的过程中,每条指令都是在cpu中执行的。程序中的临时数据都是存放在主存中的。而cpu和数据直接产生交互的是高速缓存。
在程序运行过程中,会将运算需要的数据从主存拷贝一份到高速缓存中。
那么cup在进行计算时直接可以从高速缓存中读取数据和写入数据,运算结束后,再将高速缓存中的数据刷新到主存。
很典型的一个例子:
i = i+1;
当线程执行这个语句时,cpu首先会先从主存中读取i的值,然后复制到高速缓存,然后cpu会对i进行+1的操作,然后写入高速缓存,最后刷新到主存中。
缓存一致性问题
这个在单线程执行时看似是没有问题的,但是在多线程的情况下,就会出现缓存不一致的问题。
比如有两个线程,一开始的时候,每个线程会将读取的i复制到自己的高速缓存中,线程1 对i进行了+1的操作,然后把i写入到内存,这时线程2中的高速缓存里i仍然为0,同时也对i进行了+1的操作,然后写入内存,这时最终的结果仍为1。我们期望的结果是2,但是最终结果为1 ,这种情况就是著名的CPU缓存一致性问题。也通常称之为多个线程访问的变量为共享变量。这种情况一般出现在多线程编程。
如何解决缓存一致性问题?
为了解决缓存一致性,通常有两种解决方案。
- 总线锁机制
- 缓存一致性协议
这两者都是硬件层面的解决方式
总线锁机制
早期cpu是通过对总线加锁进行处理的,因为cpu与其他组件进行通信都是通过总线进行的。如果对总线加锁,就会阻塞了其他cpu对组件的访问,从而使得只有一个cpu能使用这个变量的内存。
但是这种方式有一个很大的缺陷,就是当在对总线加锁期间,其他cpu都不能访问内存,从而导致效率低下。
缓存一致性协议
于是后来出现了一种缓存一致性协议,最出名的是intel的MESI协议,它保证每个线程的共享变量的副本是一致的。
它的核心思想是,当CPU写数据时,如果发现操作的是共享变量,即其他cpu也存在该变量的副本,会发出信号通知其他cpu将该变量的缓存行置为无效状态。因此其他cpu当读到这个变量是无效状态时,那么他就会从内存重新读取。
并发编程中三个核心概念
- 原子性
- 有序性
- 可见性
原子性
程序执行一个或者多个操作,要么同时执行成功,要么同时执行失败。
有序性
程序的执行顺序要按照代码的先后顺序执行
可见性
当多线程访问同一个变量时,一个线程修改了变量的值,对于其他线程是可见的。
其他两个大家可能都理解,重点说一下有序性:
当代码中的数据没有依赖性的时候,处理器为了提高性能,可能会对代码进行优化,他不会保证程序中各个语句的执行先后顺序和代码里的一致,但他会保证最终的结果一致。
比如:
int i = 9; //(1)
int b = 1; //(2)
int c = i+b; //(3)
其中(1)和(2)中的代码数据没有依赖,但是(3)用到了前两行的结果,所以(3)不会重排序,(1)和(2)的执行顺序不能保证。
这种情况在单线程的情况是没有问题的,但是在多线程的情况就会出现问题。
如
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
因为线程1中的语句1和语句2是没有依赖性的,所以语句1和语句2可能发生重排序。
如果线程1先执行语句2,此时线程2执行while中条件为false,会以为线程1的context初始化完成,执行doSomethingwithconfig(context),这时线程2就会报错。
由此可见,指令的重排序在单线程情况是没有问题的,但是在多线程的情况就可能出现程序的错误。
也就是说,要想保证多线程程序能够正确的执行,必须要保证程序的原子性,可见性以及有序性。如果有一个不能被保证,就可能会出现程序运行不正确。