在C++开发的后台服务、游戏服务端或客户端程序中,经常需要在不重启程序的前提下调整运行参数,比如修改日志级别、调整业务阈值、切换功能开关,这就需要实现支持热更新的配置管理系统。这类系统需要能够实时感知配置文件的变更,自动加载新配置并同步到程序的运行逻辑中,同时保证配置读取的线程安全,避免更新过程中出现数据不一致的问题。

核心设计思路
支持热更新的配置管理系统主要包含四个核心模块:
- 配置文件监听模块:负责监控配置文件的变化,当文件被修改、替换时触发更新事件。
- 配置解析模块:负责读取配置文件内容,解析成程序可识别的结构化数据,同时完成配置项的合法性校验。
- 配置存储模块:以线程安全的方式存储解析后的配置数据,供业务模块读取。
- 更新同步模块:处理配置更新事件,完成旧配置到新配置的平滑切换,必要时通知业务模块配置已变更。
配置文件监听实现
在Linux环境下,可以使用inotify机制监听文件变化,Windows环境下可以使用ReadDirectoryChangesW或者第三方库实现。下面以Linux下的inotify为例,实现配置文件监听功能。
#include <sys/inotify.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <functional>
class ConfigFileWatcher {
public:
// 定义配置更新回调函数类型
using UpdateCallback = std::function<void(const std::string&)>;
ConfigFileWatcher(const std::string& file_path, UpdateCallback cb)
: file_path_(file_path), callback_(cb), inotify_fd_(-1), watch_fd_(-1) {}
~ConfigFileWatcher() {
if (watch_fd_ != -1) {
inotify_rm_watch(inotify_fd_, watch_fd_);
}
if (inotify_fd_ != -1) {
close(inotify_fd_);
}
}
// 初始化监听
bool init() {
inotify_fd_ = inotify_init();
if (inotify_fd_ == -1) {
std::cerr << "inotify_init failed" << std::endl;
return false;
}
// 监听文件的修改和关闭写事件
watch_fd_ = inotify_add_watch(inotify_fd_, file_path_.c_str(), IN_MODIFY | IN_CLOSE_WRITE);
if (watch_fd_ == -1) {
std::cerr << "inotify_add_watch failed for file: " << file_path_ << std::endl;
close(inotify_fd_);
inotify_fd_ = -1;
return false;
}
return true;
}
// 阻塞监听文件变化,建议在独立线程中运行
void run() {
if (inotify_fd_ == -1) return;
char buffer[4096];
while (true) {
ssize_t len = read(inotify_fd_, buffer, sizeof(buffer));
if (len <= 0) continue;
// 解析inotify事件
struct inotify_event* event;
for (char* ptr = buffer; ptr < buffer + len; ptr += sizeof(struct inotify_event) + event->len) {
event = (struct inotify_event*)ptr;
if (event->mask & IN_MODIFY || event->mask & IN_CLOSE_WRITE) {
// 触发配置更新回调
if (callback_) {
callback_(file_path_);
}
}
}
}
}
private:
std::string file_path_; // 监控的配置文件路径
UpdateCallback callback_; // 配置更新回调函数
int inotify_fd_; // inotify实例文件描述符
int watch_fd_; // 监控项描述符
};
配置解析与存储实现
配置文件可以选择JSON、YAML、INI等格式,这里以简单的INI格式为例,实现配置的解析和线程安全的存储。使用std::shared_mutex实现读写锁,保证多读单写的线程安全。
#include <unordered_map>
#include <string>
#include <fstream>
#include <sstream>
#include <mutex>
#include <shared_mutex>
#include <iostream>
class ConfigManager {
public:
ConfigManager() = default;
// 加载并解析配置文件
bool load_config(const std::string& file_path) {
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> new_config;
std::ifstream file(file_path);
if (!file.is_open()) {
std::cerr << "open config file failed: " << file_path << std::endl;
return false;
}
std::string line;
std::string current_section;
while (std::getline(file, line)) {
// 去除首尾空格
line.erase(0, line.find_first_not_of(" t"));
line.erase(line.find_last_not_of(" t") + 1);
// 跳过空行和注释
if (line.empty() || line[0] == '#' || line[0] == ';') continue;
// 处理section
if (line[0] == '[' && line.back() == ']') {
current_section = line.substr(1, line.size() - 2);
continue;
}
// 处理键值对
size_t pos = line.find('=');
if (pos != std::string::npos) {
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// 去除键值对的前后空格
key.erase(0, key.find_first_not_of(" t"));
key.erase(key.find_last_not_of(" t") + 1);
value.erase(0, value.find_first_not_of(" t"));
value.erase(value.find_last_not_of(" t") + 1);
new_config[current_section][key] = value;
}
}
// 校验配置合法性(示例:检查必要配置项是否存在)
if (new_config["common"].find("log_level") == new_config["common"].end()) {
std::cerr << "missing required config: common.log_level" << std::endl;
return false;
}
// 加写锁更新配置
std::unique_lock<std::shared_mutex> lock(mutex_);
config_ = std::move(new_config);
std::cout << "config reload success" << std::endl;
return true;
}
// 获取配置项,支持指定section和key,默认返回空字符串
std::string get_config(const std::string& section, const std::string& key) {
// 加读锁读取配置
std::shared_lock<std::shared_mutex> lock(mutex_);
auto section_it = config_.find(section);
if (section_it == config_.end()) return "";
auto key_it = section_it->second.find(key);
if (key_it == section_it->second.end()) return "";
return key_it->second;
}
// 获取int类型的配置项,转换失败返回默认值
int get_config_int(const std::string& section, const std::string& key, int default_val = 0) {
std::string val = get_config(section, key);
if (val.empty()) return default_val;
try {
return std::stoi(val);
} catch (...) {
return default_val;
}
}
private:
// 配置存储结构:section -> key -> value
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> config_;
std::shared_mutex mutex_; // 读写锁,保证线程安全
};
完整整合示例
将文件监听和配置管理模块整合,实现完整的支持热更新的配置管理系统,在独立线程中运行文件监听,当配置文件变化时自动重新加载。
#include <thread>
#include <iostream>
// 全局配置管理器实例
ConfigManager g_config_mgr;
// 配置更新回调函数
void on_config_update(const std::string& file_path) {
std::cout << "detect config file change, reload now" << std::endl;
g_config_mgr.load_config(file_path);
}
int main() {
std::string config_path = "./config.ini";
// 初始加载配置
if (!g_config_mgr.load_config(config_path)) {
std::cerr << "init config failed" << std::endl;
return -1;
}
// 创建文件监听器
ConfigFileWatcher watcher(config_path, on_config_update);
if (!watcher.init()) {
std::cerr << "init config watcher failed" << std::endl;
return -1;
}
// 启动监听线程
std::thread watch_thread([&watcher]() {
watcher.run();
});
// 模拟业务线程读取配置
std::thread business_thread([]() {
while (true) {
int log_level = g_config_mgr.get_config_int("common", "log_level", 1);
std::cout << "current log level: " << log_level << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
});
watch_thread.join();
business_thread.join();
return 0;
}
注意事项
- 配置文件解析时要做好异常处理,避免格式错误导致程序崩溃。
- 配置更新时如果需要通知业务模块,可以额外实现配置变更回调列表,在加载新配置后触发所有回调。
- 如果配置项有依赖关系,需要在加载时校验依赖的合法性,避免更新后出现逻辑错误。
- 对于高频修改的配置文件,可以增加防抖逻辑,避免短时间内多次修改触发多次重复加载。