说到高并发场景下的数据结构保护,大多数人想到的是互斥锁(Mutex)或读写锁(RWLock)。但Linux内核里有一个更优雅的方案——RCU(Read-Copy-Update),它在读多写少的场景下能提供接近零开销的读取性能;
这篇文章把RCU的原理讲清楚,然后用用户态的实现让你在自己的项目里也能用上它;
RCU解决什么问题
考虑一个典型的场景:一个配置缓存,被大量goroutine/g线程频繁读取,偶尔被管理员更新。用读写锁的话,读操作之间虽然不互斥,但读操作和写操作互斥——写操作会阻塞所有正在进行的读操作;
RCU的思路完全不同:读操作完全不加锁,写操作创建新版本的数据,等所有旧读者都退出后再回收旧数据。结果是读操作的性能开销趋近于零。
核心原理
RCU的核心是"宽限期"(Grace Period)的概念。整个过程是这样的:
读取阶段:读者通过rcu_read_lock()进入临界区,通过指针访问数据,通过rcu_read_unlock()退出。整个过程不加任何锁,不阻塞任何其他操作;
更新阶段:写者分配新数据,复制旧数据的内容,修改需要变更的部分,然后用原子操作把指向旧数据的指针替换为指向新数据的指针。这个替换操作是瞬间完成的,不会阻塞读者;
回收阶段:旧数据不能立即释放——可能还有读者正在使用它。写者调用synchronize_rcu()等待一个宽限期,确保所有在指针替换之前就开始读取的读者都已经退出临界区。宽限期结束后,旧数据可以安全释放;
用户态Python实现
Linux内核的RCU是用C实现的,但在用户态我们可以用Python做一个简化的模拟:
import threading
import copy
import time
from typing import TypeVar, Generic
T = TypeVar('T')
class RCUProtected(Generic[T]):
"""RCU保护的数据结构"""
def __init__(self, data: T):
self._data = data
self._lock = threading.Lock()
self._reader_count = 0
self._reader_lock = threading.Lock()
def read_lock(self):
"""进入读临界区"""
with self._reader_lock:
self._reader_count += 1
def read_unlock(self):
"""退出读临界区"""
with self._reader_lock:
self._reader_count -= 1
def read(self) -> T:
"""读取当前数据(不阻塞其他读者)"""
return self._data
def update(self, new_data: T):
"""更新数据(创建新副本)"""
with self._lock:
old_data = self._data
self._data = new_data
# 等待所有旧读者退出
while True:
with self._reader_lock:
if self._reader_count == 0:
break
time.sleep(0.001)
# 旧数据现在可以安全回收了
del old_data
使用方式:
config = RCUProtected({"timeout": 30, "max_retries": 3})
# 读者(高频,无锁)
def reader_work():
config.read_lock()
try:
data = config.read()
# 使用data...
finally:
config.read_unlock()
# 写者(低频,等待宽限期后回收旧数据)
def update_config(new_config):
config.update(new_config)
Go语言实现
Go的runtime里内置了RCU的语义支持,配合atomic包可以写出更高效的实现:
type RCUValue[T any] struct {
ptr atomic.Pointer[T]
}
func (r *RCUValue[T]) Read() *T {
return r.ptr.Load() // 原子读,无锁
}
func (r *RCUValue[T]) Update(newVal *T) {
old := r.ptr.Swap(newVal)
// 等待宽限期后释放old
runtime.GC() // Go的GC天然提供了宽限期语义
_ = old
}
适用场景
RCU最适合的场景是读多写少(读写比1000:1以上)、读操作延迟敏感、数据结构不太大(复制开销可接受)。典型的例子包括配置缓存、路由表、DNS缓存、白名单/黑名单;
不适合的场景是写操作频繁(每次更新都要等待宽限期+复制数据)、数据结构很大(复制开销太高)、需要强一致性保证;
写在最后
RCU是Linux内核中最高级的同步原语之一,但核心思想并不复杂——“读不加锁,写创建新版本,等旧读者退出再回收”。理解这个思路后,你会发现它在很多业务场景下都能派上用场。并发编程不只有锁这一条路。