说到高并发场景下的数据结构保护,大多数人想到的是互斥锁(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内核中最高级的同步原语之一,但核心思想并不复杂——“读不加锁,写创建新版本,等旧读者退出再回收”。理解这个思路后,你会发现它在很多业务场景下都能派上用场。并发编程不只有锁这一条路。