最近在看envoy的时候,里面有提到一个RCU的概念,发现这种同步机制在前前公司的项目中经常使用在前前公司的项目中,有配置读取这样多读少写的情况,删减之后的核心代码如下:

  • 最新的配置直接使用原子变量指针,由于只有一个线程写入,访问old_server_config_ 无需加锁
  • 每次GetServerConfig时,都是获取的最新配置,旧的配置内存没被回收,也可以继续访问
  • 这种实现形式避免了加互斥锁或者读写锁,基本对读性能没有影响
template <typename Config>
class ServerConfigManager {
public:
    virtual ~ServerConfigManager() {
        std::for_each(old_server_config_.begin(), old_server_config_.end(),
                      [](Config*& config) { delete config; });
        delete server_config_;
    }
    // 获取当前服务配置
    Config* GetServerConfig() {
        Config* config = server_config_.load(std::memory_order_acquire);
        return config;
    }
    virtual Config* NewConfig() { return new Config(); }

    virtual bool reset_value() {
        Config* config = NewConfig();
        // read config
        // do something
        Config* old_config = server_config_.exchange(config, std::memory_order_acq_rel);
        if (old_config != nullptr) /* Already inited. */ {
            old_server_config_.push_back(old_config);
        }
        return true;
    }
private:
    std::atomic<Config*> server_config_{nullptr};
    std::vector<Config*> old_server_config_;
    std::vector<std::string> file_names_;
};

在配置支持热更新的这种场景下,是可以接收服务当前请求使用的是旧的配置,在下次请求时,会使用新的配置以上其实就是RCU概念的简化实现。对比linux内核中的实现缺少同步操作和回收操作这种RCU的机制在很多地方都有应用,具体可以看参考资料维基百科的介绍

在带GC的语言中,配置热更的实现会更简单,例如在go中:

type Config struct {
}

var config atomic.Pointer[Config]

func UpdateConfig() {
	// read config
	cfg := Config{}
	config.Store(&cfg)
}

func GetConfig() *Config {
	return config.Load()
}

在刷知乎(参考资料3)的时候,还偶然看到C++26增加了RCU的实现
C++ 26 RCU

参考资料