在应用层,对于锁的应用大家应该都很相熟了, 作用就是为了爱护共享变量不被同时操作而导致无奈预测的状况。然而深刻到具体实现,锁仅仅只是锁定临界区吗?
锁的实现其实还必须实现一个语义,也就是 内存屏障。内存屏障次要用于避免指令重排而导致的无奈预测的状况。代码通过编译器生成的指令并不一定都是按着咱们原先的想法来生成的,可能通过优化等状况进行了指令的重排,然而这些重排在执行后的后果该当是统一的。其实及时编译器不重排指令,在古代的 cpu 中,也经常会将指令乱序执行,所以内存屏障能够保障屏障指令前后的指令程序。
内存屏障也分为读屏障 (rmb) 与写屏障(wmb)。这些读写屏障次要用于在多核 cpu 的情景下能够强制同步 cpu 中缓存不统一的状况。
这些又牵扯到了多 cpu 中缓存一致性的问题。假如只有一个 cpu,那么 cpu 只会从本人的缓存中读数据,如果产生了缓存 miss,则会从主存中读取数据到缓存中,所以 cpu 无论在何时看到的最终内存数据都是统一的。
然而在多核状况下,就不是这么简略的了。每个 cpu 都有本人的缓存,每个 cpu 最终看到的数据,就是不在缓存中的主存 + 已在缓存中的数据。所以假如多 cpu 的状况下,某个 cpu 更新了某个 cache line 中的值又没有回写到内存中,那么其它 cpu 中的数据其实曾经是旧的已作废的数据,这是不可承受的。
为了解决这种状况,引入了缓存一致性协定,其中用的比拟多的称为MESI,别离是 cache line 可能存在的四种状态:
- Modified。数据已读入 cache line,并且曾经被批改过了。该 cpu 领有最新的数据,能够间接批改数据。当其它外围须要读取相应数据的时候,此数据必须刷入主存。
- Exclusive。数据已读入 cache line,并且只有该 cpu 领有它。该 cpu 能够间接批改数据,然而该数据与主存中数据是统一的。
- Shared。多个 cpu 共享某内存的数据,可能由 Exclusive 状态扭转而来,当某个 cpu 须要批改数据的时候,必须提交 RFO 申请来获取数据的独占权,而后能力进行批改。
- Invalid。有效的 cache line,和没有载入一样。当某个 cpu 的 cache line 处于 Shared 状态,别的 cpu 申请写的时候,接管了 RFO 申请后会变为此种状态。
这四种状态能够一直的扭转,有了这套协定,不同的 cpu 之间的缓存就能够保证数据的一致性了。然而依赖这套协定,会大大的升高性能,比方一个外围上某个 Shared 的 cache line 打算写,则必须先 RFO 来获取独占权,当其它外围确认了之后能力转为 Exclusive 状态来进行批改,假如其余的外围正在解决别的事件而导致一段时间后才回应,则会当申请 RFO 的外围处于无事可做的状态,这是不可承受的。
于是在每个 cpu 中,又退出了两个相似于缓存的货色,别离称为 Store buffer 与Invalidate queue。
Store buffer 用于缓存写指令,当 cpu 须要写 cache line 的时候,并不会执行上述的流程,而是将写指令丢入 Store buffer,当收到其它外围的 RFO 回应后,该指令才会真正执行。
Invalidate queue 用于缓存 Shared->Invalid 状态的指令,当 cpu 收到其它外围的 RFO 指令后,会将本身对应的 cache line 有效化,然而当外围比较忙的时候,无奈立即解决,所以引入 Invalidate queue,当收到 RFO 指令后,立即回应,将有效化的指令投入 Invalidate queue。
这套机制大大晋升了性能,然而很多操作其实也就异步化了,某个 cpu 写入了货色,则该写入可能只对以后 CPU 可见(读缓存机制会先读 Store buffer,再读缓存),而其余的 cpu 可能无奈感知到内存产生了扭转,即便 Invalidate queue 中已有该有效化指令。
为了解决这个问题,引入了读写屏障。写屏障次要保障在写屏障之前的在 Store buffer 中的指令都真正的写入了缓存,读屏障次要保障了在读屏障之前所有 Invalidate queue 中所有的有效化指令都执行。有了读写屏障的配合,那么在不同的外围上,缓存能够失去强同步。
所以在锁的实现上,个别 lock 都会退出读屏障,保障后续代码能够读到别的 cpu 外围上的未回写的缓存数据,而 unlock 都会退出写屏障,将所有的未回写的缓存进行回写。
参考: https://wudaijun.com/2019/04/…