本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的根底上,联合 OpenJDK 11 以上的版本的代码进行了解和实现。并依据集体的查资料以及了解的经验,给各位想更深刻了解的人分享一些集体的材料
硬件根底
处理器和线程(processors and threads)
多处理器 (multiprocessor)包含多个硬件处理器,每个都能执行 一个顺序程序 。当探讨多处理器架构的时候,根本的工夫单位是 指令周期(cycle):即处理器提取和执行一条指令须要的工夫。
线程 是一个 顺序程序 ,是一个软件形象。 上下文切换(context switch)指的是处理器能够执行一个线程一段时间之后去执行另一个线程。处理器能够因为各种起因撤销一个线程或者从调度中删除该线程:
- 线程收回了一个内存申请,而该申请须要一段时间能力实现
- 线程曾经运行了足够长的工夫,该让别的线程执行了。
当线程被从调度中删除时,他可能从新在另一个处理器上执行。
互连线(interconnect)
目前常见的三种服务器根本互联构造:
- SMP(symmetric multiprocessing,对称多解决)
- NUMA(nonuniform memory access,非统一内存拜访)
SMP 指多个 CPU 对称工作,无主次或从属关系。各 CPU 共享雷同的物理内存。每个 CPU 拜访内存中的任何地址 所需工夫是雷同的 ,因而 SMP 也被称为 统一存储器拜访构造 (即 UMA:Uniform Memory Access)。个别 SMP 架构中,CPU 和内存之间存在高速缓存。并且,处理器和主存都有用来负责发送和监听总线上播送信息的 总线管制单元(bus controller)。整体构造如下图所示:
这种构造最为容易实现,然而随着处理器的增多,总线并不能扩大导致总线终将过载。
在 NUMA 系统结构中,与 SMP 相同,一系列节点通过点对点网络相互连贯,有点像一个小型的局域网,每个节点蕴含若干个处理器和本地内存 。一个节点的本地存储 对于其余节点也是能够拜访的,当然,拜访本人的本地内存要快于拜访其余节点的内存。网络比总线简单,须要更加简单的协定,然而带来了扩展性。如下图所示:
从程序员的角度看,无论底层是 SMP 还是 NUMA,互连线都是无限的资源。写代码的时候,要思考这一点防止应用过多的互联线资源。
内存(memory)
所有处理器共享内存,通常会被形象成为一个很大的“字 ”(words)数组,数组下标即为 地址(address)。字长度和平台相干,当初多为 64 位,地址的最大长度也是这么长。64 位能示意的内存就曾经很大了。
处理器拜访内存的流程,简略概括包含:
- 处理器通过给内存 发送一个蕴含要读取的地址的音讯,来获取内存上对应地址的值
- 处理器通过给内存 发送一个蕴含要写入的地址和值的音讯 , 数据写入后,内存回复一个确认音讯。
高速缓存(Cache)
缓存命中率
如果处理器始终间接从内存中读取,处理器间接拜访内存耗费工夫很长,可能须要 几百个指令周期 ,这样效率会很低。个别须要引入 若干个高速缓存(Cache):与处理器紧挨着的小型存储器,位于处理器和内存之间。
当须要读取一个地址的值时,拜访高速缓存看是否存在:存在代表 命中 (hit),间接读取。不存在被称为 缺失(miss)。同样的,如果须要写一个值到一个地址,这个地址在缓存中存在也就不须要拜访内存了。
咱们个别比较关心高速缓存中命中的申请比例,也就是 缓存命中率
局部性与缓存行
大部分程序都体现出较高的 局部性(locality):
- 如果处理器读或写一个内存地址,那么它很可能很快还会读或写同一个地址。
- 如果处理器读或写一个内存地址,那么它很可能很快还会读或写左近的地址。
针对局部性,高速缓存个别会一次操作不止一个字,而是 一组邻近的字 ,称为 缓存行。
多级高速缓存
古代处理器中个别不止一级缓存,而是多级缓存,从离处理器最近到最远别离是 L1 Cache,L2 Cache 和 L3 Cache:
- L1 Cache 通常和处理器位于同一个芯片,离处理器最近,拜访仅须要 1~3 个指令周期
- L2 Cache 通常和处理器位于同一个芯片,处于边缓地位,拜访须要通过更远的铜线,甚至更多的电路,从而减少了延时,个别在 8 ~ 11 个指令周期左右
- L3 Cache L1/L2 为每个处理器公有的,这样导致对于很多雷同的数据,也只能每个处理器独有的缓存各保留一份。所以须要思考引入一个所有处理器共用的缓存,这就是 L3 缓存。L3 缓存的材质以及布线都和 L1/L2 不同,须要更长的工夫拜访,个别在 20 ~ 25 个指令周期左右
高速缓存内存无限,在同一时刻只有一部分内存单元被搁置在高速缓存中,因而咱们须要缓存 替换策略 。如果 替换策略 能够替换任何缓存行,则该高速缓存是 全相联 (fully associative) 的。相同,如果只能替换一个特定的缓存行,他就是 间接映射 (direct mapped) 的。如果取其折中,即容许应用一组大小为 k 的汇合中任一缓存行来替换,则称为 k 级组相联(k-way set associative) 的。
一致性(coherence)
当一个处理器拜访另一个处理器曾经装载入高速缓存的主存地址的时候,就会产生 共享 (sharing,或者称为 争用 contention)。须要思考缓存一致性的问题,因为如果一个处理器要更新共享的缓存行,则另一个处理器的正本须要作废免得读取到过期的值。
MESI 缓存一致性协定,缓存行存在以下四种状态:
- Modified:缓存行被批改,最终肯定会被写回入主存,在此之前其余处理器不能再缓存这个缓存行。
- Exclusive:缓存行还未被批改,然而其余的处理器不能将这个缓存行载入缓存
- Shared:缓存行未被批改,其余处理器能够加载这个缓存行到缓存
- Invalid:缓存行中没有有意义的数据
举例:假如处理器和主存由总线连贯,如图所示:
a) 处理器 A 从地址 a 读取数据,将数据存入他的高速缓存并置为 Exclusive
b) 处理器 B 从地址 a 读取数据,处理器 A 检测到地址抵触,响应缓存中 a 地址的数据,之后,地址 a 的数据被 A 和 B 以 Shared 状态装入缓存
c) 处理器 B 对于 a 进行写操作,状态批改为 Modified,并播送揭示 A(所有其余曾经将该数据装入缓存的处理器),状态置为 Invalid。
d) 随后 A 还须要拜访 a,它会播送这个申请,B 将批改过的数据发到 A 和主存上,并且置两个正本状态为 Shared。
当处理器拜访逻辑上不同的数据,然而这些数据恰好处于同一内存行,这种状况被称为 谬误共享(false sharing)
自旋(Spinning)
自旋 即:某个处理器一直地查看内存中的某个字,期待另一个处理器扭转它。
对于具备高速缓存的 SMP 或者 NUMA 系统结构,自旋仅耗费非常少的资源。依据下面咱们对于 MESI 的介绍,第一次读取地址时,会产生一个高速缓存缺失,将该地址的内容加载到缓存块中。尔后,只有数据没有扭转,处理器仅从高速缓存读取数据,不须要占用互连线。当这个地址被批改时,处理器也会接管到 Invalid 并且从新申请这个数据并获取到批改。
为何 TTASLock 要优于 TASLock。
通过之前的剖析,咱们能够晓得,TASLock 的每次 LOCKED.compareAndSet(this, false, true)
的时候,都会产生批改信号,占用互连线带宽。while 循环每次都执行,会产生大量批改信号。然而 TTASLock 的 LOCKED.get(this)
仅仅是一次本地自旋。所以 TTASLock 要比 TASLock 性能快得多。