之前我曾写过关于MESI的一篇文章,大体了解了下CPU在多核心下如何同步不同Cache中的数据的,尽可能在一些脏读的情况下尽量的提高性能,其中最主要的两个概念分别为Store bufferInvalidate queue

借助上述两个概念,可以无需等待对应操作的真正执行就可以继续执行下去了,大大提升了CPU的执行效率,而不是耗费在无意义的等待状态。

我们也知道,使用Memory barrier可以强制的清空Store bufferInvalidate queue,使得某个内存在不同的CPU Cache中均保持最新版本,从而避免脏读。

前几天研究RocksDB源码的时候突然想到了一个问题,一般来说,假如使用松散的内存模型(C++中的memory_order_relaxed)的话,对于一个线程中,该值永远是可见的,而对于其它线程,可能会有存在脏读的时间段(Invalidate queue没有被清空的清空下,存在着短暂的时间窗口)。

然而假设该线程在修改了该变量后,在CPU2的Invalidate queue没有被应用之前,被调度至CPU2执行,同时CPU2中的Cache中也有对应地址的Cache line并且处于Shared状态,会不会存在脏读呢?

假设操作系统没有做出任何的处理,那么我感觉是可能会出现上述的场景的,该场景会导致使用memory_order_relaxed内存序的写入的时候,正好被切换至CPU2进行调度了,那么岂不是会产生脏读,然后就产生了问题?

结论肯定是不会的,但是为何会如此呢?

通过查阅资料,找到了如下的靠谱解释,由于直接引用了linux的调度源码,所以应该是正确的解释,解释了为什么在一个线程调度至另一个线程之后,松散的内存序还可以保证当前线程不会发生脏读的清空。

首先上下线程调度的源码:

static void __sched notrace __schedule(bool preempt)
{
     struct task_struct *prev, *next;
     unsigned long *switch_count;
     struct rq_flags rf;
     struct rq *rq;
     int cpu;

     cpu = smp_processor_id();
     rq = cpu_rq(cpu);
     prev = rq->curr;

     schedule_debug(prev);

     if (sched_feat(HRTICK))
         hrtick_clear(rq);

     local_irq_disable();
     rcu_note_context_switch(preempt);

     rq_lock(rq, &rf);            // Note
     smp_mb__after_spinlock();    // Note

     ...
}

我们可以看到关键的smp_mb__after_spinlock,会刷新缓存,也就是回写Store buffer值,然后发送Invalidate消息至其它核心。

当另一个CPU核心要调度此线程的时候,也需要执行该函数。执行后会清空Invalidate queue,将对应的Cache line设置为Invalid状态。当线程访问对应内存的时候,会将此Cache line重新从内存中读取,并设置为Shared状态。

由此可见,即使使用弱内存序的操作,对于同一个线程来说,即使遇到了CPU的切换,也不会影响数据的可见性,不会产生脏读。

共 2 条回复
  
[email protected]   (游客) 2021-10-10 09:44
楼主探究的挺细嘛 我之前修过的同步问题 也就是一个Map::Update提交了传送请求至另一Map后 自己没有释放 对面就开始维护 导致的bug 远到不了CPU Cache级别
  
[email protected]   (游客) 14 天前
感谢这篇文章,最近也遇到了这个疑惑,问 了多个 AI,包括 gpt4-turbo,统一都说会存在脏读的情况,意思是多个线程使用 memory_order_relaxed 对计数的原子变量操作时,例如 fetch_add,但使用 memory_order_relaxed,会导致计数不符合预期,因为 memory_order_relaxed 并没保证可见性,但是看 stackoverflow 和很多技术文章都说无问题
发表新回复

作者

sryan
today is a good day