在多核CPU时代,多线程成为一种更高效的编程方式,为了更好的利用多线程编程,我们有必要了解和学习linux内存模型。
1 Linux中的cache
讲cache之前,简单了解一下程序是怎么在机器运行的, linux环境下程序是运行在 RAM之中(默认都知道啊),RAM 就是所谓的DDR,称之为main memory(主存)。当我们在Linux环境下启动一个线程时,操作系统会进行调度,从磁盘中将可执行文件加载到主存中,然后开始运行程序,这个运行过程中会涉及到主存和CPU之间交互,为了方便叙述,我们用一个简单的例子说明这个程序执行过程,代码如下:
int a=0;a=a+1;
如果CPU需要将一个变量a(因为实际是内存操作,我们假设地址是A啊)加1,一般分为以下3个步骤:
- CPU 从主存中读取地址A的数据到内部通用寄存器 x0。
- 通用寄存器 x0 加1。
- CPU 将通用寄存器 x0 的值写入主存。
整个过程如下图所示:
图1
上图显示的只是一个理想过程,理想是丰满的,但是现实是骨感的啊,其实现实中,CPU通用寄存器的速度和主存之间存在着太大的差异。两者之间的速度差距很大,CPU register的速度一般小于1ns,主存的速度一般是65ns左右。速度差异近百倍啊。因此,上面举例的3个步骤中,步骤1和步骤3实际上速度很慢。当CPU试图从主存中load/store 操作时,由于主存的速度限制,CPU不得不等待这漫长的65ns时间。所以为了提升处理效率,我们有必要提升主存的速度,出于成本和效率考虑,人们考虑在cpu与主存之间加入缓存在硬件上,我们将cache放置在CPU和主存之间,作为主存数据的缓存。当CPU试图从主存中load/store数据的时候, CPU会首先从cache中查找对应地址的数据是否缓存在cache中。如果数据缓存在cache中,直接从cache中拿到数据并返回给CPU。当存在cache的时候,程序运行流程如下:
图2
有没有感觉redis就是和这玩意差不多呢,事实上cache的花样很多,上图只是一个简单模式,事实上现代多核CPU都长成下图这样:
图3
但是,这就满足人们对性能的追求了吗?并没有。为了进一步提升性能,引入多级cache。前面提到的cache,称之为L1 cache(第一级cache)。我们在L1 cache 后面连接L2 cache,在L2 cache 和主存之间连接L3 cache。等级越高,速度越慢,容量越大,每个CPU也都有自己内部才能访问的缓存,结构变成了图3所示的这个样子啊,执行如下:
- 有多个CPU处理器,每个CPU处理器内部又有多个核心。
- 存在只能被一个CPU核心访问的L1 cache。
- 存在只能被一个CPU处理器的多个核心访问的L2 cache。
- 存在能被所有CPU处理器都能访问到的L3 cache以及内存。
- L1 cache、L2 cache、L3 cache的容量空间依次变大,但是访问速度依次变慢。
写到这你可能在想,这个缓存的结构和我们写c++代码有啥关系呢?我们考虑一种c++多线程的情况,不同的线程在不同核心运行时,这些缓存的存在会给数据同步带来挑战,当CPU结构发生变化,增加了只能由内部才能访问的缓存之后,一些在旧架构上不会出现的问题,在新的架构上就会出现。而本篇的主角内存模型(memory model),其作用就是规定了各种不同的访问共享内存的方式,不同的内存模型,既需要编译器的支持,也需要硬件CPU的支持。
2 从c++多线程访问数据开始
我们从一个c++多线程处理数据开始引入多线程数据同步问题:
#include#include #include #include int A,B; void FuncA(){
A=1; std::cout<} void FuncB(){
B=2; std::cout<} int main(){
A=0; B=0; std::thread thread1(FuncA); std::thread thread2(FuncB); thread1.join(); thread2.join(); return 0; }
上图的程序主要做了这样的实验,程序执行之前,A=B=0,有两个线程同时分别执行如下的代码:
线程1(thread1) | 线程2(thread2) |
---|