把更新与回收分开
读—复制—更新,简称RCU,是并发计算中的同步机制,常用于读多写少的数据结构。它不要求修改者先把全部读者挡住,而是把对象退出共享结构与释放其内存分成两步。数据已经从查找路径上移除,不等于没有线程还拿着指向它的指针。
以下以Linux内核的RCU接口说明这一思想。读者用rcu_read_lock和rcu_read_unlock标记读侧临界区,在适当保护下访问对象。名称中虽有“锁”,其作用并不是取得与写者互斥的普通读锁;读者和更新者可以同时前进。具体实现、可抢占性和调用约束不能由这一名称一概推定。
一次链表替换
设链表依次连接A、B、C,需要改变B的内容。写者可准备B的新副本,完成字段及后继链接的初始化,再用适当的RCU链表替换操作把新副本接入。已经取得旧B的读者继续沿旧对象访问;随后沿更新路径查找的读者可遇到新B。Linux的list_replace_rcu保证遍历者在该替换处看到旧元素或新元素,而不是两者都看见或两者都漏掉。
替换后不能立即释放旧B。写者须等待一个宽限期,确认此前已经开始、可能持有旧B的读侧临界区全部结束,才可回收。宽限期不是随意睡眠若干毫秒,也不要求所有未来读者都停止。同步方式可调用synchronize_rcu等待;异步方式可登记call_rcu回调,让回收在条件满足后进行。
看得见与活得够久
对象存活与数据可见性是两个问题。即使内存尚未释放,若编译器或处理器把发布指针排到字段初始化之前,读者仍可能通过新指针读到未准备好的内容。Linux以rcu_assign_pointer发布指针,并要求读者按规则使用rcu_dereference取得它,使初始化和访问满足必要的顺序约束。
因此,宽限期不能替代发布协议,发布协议也不能替代延迟回收。读者若要连续取同一对象的多个字段,应保留取得的对象指针,而不是反复读取可能被更新的共享指针。离开保护范围后继续使用该指针也不安全,除非已转交给引用计数或其他适当的存活保障。
低开销不等于处处无锁
读者少争用共享锁,是RCU的主要吸引力;代价则包括旧版本暂留、复制以及等待回收。更新者之间仍可能需要互斥锁或自旋锁,防止并发改链造成损坏。Linux文档的审计规则示例看似删去了写锁,原因是调用者已经持有另一把互斥锁,而不是RCU自动解决了写写冲突。
RCU遍历也不自动提供整个结构在某一瞬间的一致快照。遍历期间增删的元素可能被看到,也可能不被看到;能否容忍这种陈旧视图取决于应用。必须排除旧状态时,还要配合状态检查或额外同步。选择RCU,是重新安排读、改、回收之间的责任,而非承诺全部操作无锁。