查看原文
其他

RCU的那点事

我是可乐 可乐 2022-09-06

阅读背景:《锁,知其然知其所以然


上一篇介绍了锁的原理和作用,在最后提到了RCU(Read-copy-update)。对于读多写少的场景,RCU可以解决读写锁的很多弊端。当然,不得不承认,RCU本身还是得依赖于锁。RCU的实现,实质上是使用更多的空间,换来时间上的减少,也就是以空间换时间。事实上,在计算机科学中,时间与空间的权衡(Space–time tradeoff)是无处不在的。


对于读写锁,不可避免会遇到下面这些问题:


  1. 当有线程占有写锁的时候,任何其他线程都不能再获取到读锁或写锁。


  2. 当有线程占有读锁的时候,任何其他线程可以获取到读锁,但不能有任何线程获取到写锁去修改临界区。


  3. 资源已经被加上读锁时,如果有线程尝试获取写锁,需要尽快得到满足,避免写锁饥饿。


对于以上的问题,RCU都能很好的避开。


  1. RCU可以任意获取读锁,也就是对临界区的读操作总是可以得到满足的。


  2. RCU写的动作实现上是先拷贝一份原有资源,然后在拷贝的备份上做修改,最后将指针指向新的资源,并在宽限期后删除原值。


  3. 临界区的原始值为A,如果有线程拿到写锁修改了临界区为B,则在写锁修改临界区之后拿到的读锁获取的临界区的值为B,之前获取的为A,这些操作通过原子操作保证。


不难看出,被RCU保护的资源,可以在更新或者删除的同时,依然可以被正确读取。原理其实也很简单,通过延迟更新或者删除资源,使得读数据的线程完全感知不到加锁操作。


RCU资源是如何生成的呢?



以指针gp指向共享资源,初始值为NULL。此时无论哪个线程获取资源,都知道资源为空,不会有任何问题。此时某个线程可以在堆中申请一块资源空间,这会儿只有当前线程可以看到这块资源。也就是说,当前线程可以肆无忌惮的蹂躏这一块内存。当然,线程一般不会这么无聊,此时一般是做一些初始化以及赋值操作。完成这些之后,就需要把当前资源的地址赋值给gp了,但是需要注意的是,这里指针赋值需要使用专门的原语rcu_assign_pointer,它可以保证赋值的原子性。


RCU资源是如何删除的呢?



最开始有这样一条链表数据,现在想要删除node_2这个节点,地址为p。一般的数据删除操作会是连续的几条指令,摘链、修改指针、释放内存,这种对于有多个读写者的情况,就可能出现问题。



RCU操作则会首先修改节点前的指针,使其指向后一个节点,这样就不会有新的线程可以获取到node_2节点的资源了。当然,这个时候node_2节点指向下一个节点的指针仍然是完好无损的,这样其他占有node_2资源的线程仍然可以通过node_2读取下一个资源。RCU会等待各个线程释放对node_2资源的占用,宽限期后释放这部分资源。宽限期(Grace period),指的就是从摘除指针,到可以释放资源之间的这段时间。



过了宽限期,就可以删除资源了。



RCU资源如何修改呢?



RCU的C指的就是Copy,其实就是在修改资源时,会先复制一份现有资源,这里的node_X其实就是node_2的拷贝。



修改资源的操作都是作用在node_X上的。修改后的node_X也是指向下一个节点node_3的。此时只需要找一个合适的机会,把node_1节点的指针指向node_X就可以了。



但是,此时还不能释放node_2的资源,因为还可能有其他线程正在使用node_2。因此需要等到宽限期后才能释放node_2的资源。



RCU最大的特点就是允许并发的读写操作,可以在现代化的对称多处理(Symmetrical Multi-Processing,SMP)系统中避开开销高昂的读取-修改-写入指令(read-modify-write instructions)、内存屏障(Memory barriers)以及高速缓存未命中(Cache misses)等情况。


当然,RCU的缺点也是有的。


  • RCU本身依然依赖于锁,只是使用的更少。


  • RCU需要占用更多的内存,RCU核心之一就是以空间换时间。


  • RCU只适用于以读取资源为主的情况,对于更新为主的情况并不适合。


  • 某些算法本身就不允许读写并发,RCU自然也就不被允许了。


这个世界本就没有最好的方法,有的只是权衡利弊之后的折衷。

推荐:

推荐阅读:我是一个程序员  、  锁,知其然知其所以然

想要了解更多,后台回复『RCU』获取更详细的资料。

喜欢就点『好看』或者『分享』吧!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存