在应用层,对于锁的应用大家应该都很相熟了,作用就是为了爱护共享变量不被同时操作而导致无奈预测的状况。然而深刻到具体实现,锁仅仅只是锁定临界区吗?
锁的实现其实还必须实现一个语义,也就是内存屏障。内存屏障次要用于避免指令重排而导致的无奈预测的状况。代码通过编译器生成的指令并不一定都是按着咱们原先的想法来生成的,可能通过优化等状况进行了指令的重排,然而这些重排在执行后的后果该当是统一的。其实及时编译器不重排指令,在古代的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/...