基于yaml-cpp的C++参数服务器设计2:多级参数配置

前言

  • 我们曾经在这篇文章中提出了一个实时无需编译反复读取文件就可以修改cpp参数的参数服务器设计: 基于yaml-cpp的C++参数服务器设计

    • 不需要重新编译程序
    • 不需要重启程序
    • 修改配置文件即可实时生效
    • 使用简单
  • 但是上述设计存在部分弊端,随着参数量的上升,上一版的设计要求设置的yaml文件的参数只存在于同一级,因此当参数量越来越多的时候会很难维护与阅读

yaml 复制代码
drone_pid_x_p: 1.0
drone_pid_x_i: 1.0
drone_pid_x_d: 1.0
drone_pid_y_p: 1.0
drone_pid_y_i: 1.0
drone_pid_y_d: 1.0
drone_pid_z_p: 1.0
drone_pid_z_i: 1.0
drone_pid_z_d: 1.0
  • 因此本次的修改考虑引入yaml多级参数的设计,当程序可以直接读取多级参数的配置,达到和之前一样的效果:
yaml 复制代码
drone:
  pid:
    x:
      kp: 0.12
      ki: 0.0
      kd: 0.0
    y: 
      kp: 0.1
      ki: 0.0
      kd: 0.0
    z:
	  kp: 0.1
	  ki: 0.0
	  kd: 0.0

1 实现

1-1 问题分析
  • 除了要保持和之前版本一样的需求,还需要考虑多级菜单的引入不能丢失原本高性能的需求,因此每次读取参数时候的设计必须丢弃这样的设计:
cpp 复制代码
root["robot"]["pid"]["kp"]
  • 这种方式存在两个问题:
    • 每次访问都需要多次 map 查找
    • 随着层级增加,访问复杂度线性上升
  • PS:设计的时候需要注意不要暴露YAML::Node的接口,用户无需关心其中具体实现。
1-2 解决思路
  • 为保证访问效率,本方案采用扁平化存储结构(Flattening)
cpp 复制代码
"robot.pid.kp" -> 0.3
"robot.pid.ki" -> 0.3
"robot.pid.kd" -> 0.3
  • 在加载 YAML 时,将树结构递归展开为:
cpp 复制代码
unordered_map<string, YAML::Node>
  • 这样最终单次访问可以退化为O(1)哈希查找

1-3 API接口
1-3-1 参数读取
  • 保持原有接口:
cpp 复制代码
template<typename T>
T getParameter(const std::string& key, const T& defaultValue);

1-3-2 存在性检查(新增)
  • 新增接口, 用于:
    • 防止默认值掩盖错误
    • 配置校验
    • 调试阶段检查
cpp 复制代码
bool hasParameter(const std::string& key);

1-3-3 分组访问(新增)
  • 支持按前缀获取子参数用于:
    • 动态模块加载
    • 遍历 PID / 控制轴
    • 插件式配置结构
cpp 复制代码
std::vector<std::string> getChildren(const std::string& prefix);
  • 例如:
cpp 复制代码
getChildren("drone.pid")
  • 返回:

    x, y, z


1-3-4 参数列表(新增)
  • 用于调试与可视化:
cpp 复制代码
std::vector<std::string> listParameters();

1-3-5 复杂度说明
操作 复杂度
getParameter O(1)
hasParameter O(1)
listParameters O(N)
getChildren O(N)

1-4 具体实现
  • ParamsServerUltra.hpp
cpp 复制代码
#pragma once

#include <yaml-cpp/yaml.h>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <filesystem>
#include <unordered_map>
#include <mutex>
#include <algorithm>
#include <unordered_set>

class ParamsServerUltra {
public:
    // 单例
    static ParamsServerUltra* getInstance(std::string file_path) {
        static ParamsServerUltra* instance =
            new ParamsServerUltra(file_path);
        return instance;
    }

    ParamsServerUltra(const ParamsServerUltra&) = delete;
    ParamsServerUltra& operator=(const ParamsServerUltra&) = delete;

    template<typename T>
    T getParameter(const std::string& key, const T& defaultValue) {
        std::lock_guard<std::mutex> lock(mutex_);

        try {
            checkReload();

            auto it = flatParams_.find(key);
            if (it == flatParams_.end()) {
                 std::cerr<< "[ParamsServerUltra][WARN] "<< "Parameter not found: " << key << ", using default value: " << defaultValue << std::endl;
                return defaultValue;
            }

            return it->second.as<T>();

        } catch (const std::exception& e) {
            std::cerr << "[ParamsServerUltra] error: " << e.what() << std::endl;
        }

        return defaultValue;
    }
    bool hasParameter(const std::string& key)
    {
        std::lock_guard<std::mutex> lock(mutex_);

        checkReload();

        return flatParams_.find(key) != flatParams_.end();
    }
    std::vector<std::string> listParameters()
    {
        std::lock_guard<std::mutex> lock(mutex_);

        checkReload();

        std::vector<std::string> result;
        result.reserve(flatParams_.size());

        for (const auto& [key, value] : flatParams_)
        {
            result.push_back(key);
        }

        std::sort(result.begin(),result.end());

        return result;
    }
    std::vector<std::string>getChildren(const std::string& prefix)
    {
        std::lock_guard<std::mutex> lock(mutex_);

        checkReload();

        std::unordered_set<std::string> unique;

        std::string searchPrefix = prefix.empty()? "": prefix + ".";

        for (const auto& [key, value] : flatParams_)
        {
            if (key.rfind(searchPrefix, 0) != 0)
            {
                continue;
            }

            std::string remain =key.substr(searchPrefix.size());

            size_t pos =remain.find('.');

            if (pos != std::string::npos)
            {
                unique.insert(remain.substr(0, pos));
            }
            else
            {
                unique.insert(remain);
            }
        }

        return { unique.begin(),  unique.end()};
    }
        // 修改 YAML 路径
    void setYamlFilePath(const std::string& newPath) {
        std::lock_guard<std::mutex> lock(mutex_);
        filename_ = newPath;
        loadParameters();
    }

private:
   
    // 构造
    ParamsServerUltra(const std::string& filename)
        : filename_(filename) {
        loadParameters();
    }

    // reload 检测
    void checkReload() {
        auto t = std::filesystem::last_write_time(filename_);
        if (t > lastLoadTime_) {
            loadParameters();
        }
    }

    // 加载 YAML + flatten
    void loadParameters() {
        try {
            std::ifstream file(filename_);
            if (!file.is_open()) {
                std::cerr << "[ParamsServerUltra] Cannot open file: "
                          << filename_ << std::endl;
                return;
            }

            YAML::Node root = YAML::Load(file);

            std::unordered_map<std::string, YAML::Node> newMap;
            flatten(root, "", newMap);

            flatParams_.swap(newMap);

            lastLoadTime_ = std::filesystem::last_write_time(filename_);

            std::cout << "[ParamsServerUltra] reloaded & flattened: "
                      << flatParams_.size() << " params\n";

        } catch (const YAML::Exception& e) {
            std::cerr << "[ParamsServerUltra] parse error: " << e.what() << std::endl;
        }
    }

    // YAML flatten
    void flatten(const YAML::Node& node,
                 const std::string& prefix,
                 std::unordered_map<std::string, YAML::Node>& out)
    {
        if (node.IsMap()) {
            for (auto it : node) {
                std::string key = it.first.as<std::string>();
                std::string fullKey = prefix.empty()
                    ? key
                    : prefix + "." + key;

                flatten(it.second, fullKey, out);
            }
        }
        else if (node.IsSequence()) {
            int idx = 0;
            for (auto it : node) {
                std::string fullKey = prefix + "[" + std::to_string(idx++) + "]";
                flatten(it, fullKey, out);
            }
        }
        else {
            out[prefix] = node;
        }
    }

private:
    std::string filename_;

    std::unordered_map<std::string, YAML::Node> flatParams_;

    std::filesystem::file_time_type lastLoadTime_;

    std::mutex mutex_;
};

2 测试

2-1 参数文件 config.yaml
yaml 复制代码
drone:
  pid:
    x:
      kp: 0.1
      ki: 0.0
      kd: 0.001
    y: 
      kp: 0.12
      ki: 0.0
      kd: 0.003
  odom:
    enable_t265: true
2-2 正常使用测试
cpp 复制代码
#include "../include/test_pkg/ParamsServerUltra.hpp"
#include <iostream>
#include <thread>
#include <chrono>

int main()
{
    auto* params_server =ParamsServerUltra::getInstance("src/test_pkg/params/config.yaml");

    while (true)
    {
        bool enable_t265 =params_server->getParameter<bool>("drone.odom.enable_t265",true);

        std::cout<< "enable_t265 = "<< enable_t265<< std::endl;

        // 打印全部参数
        for (const auto& key : params_server->listParameters())
        {
            std::cout << key << std::endl;
        }

        // 获取 pid 轴
        auto axes =params_server->getChildren("drone.pid");

        for (const auto& axis : axes)
        {
            double kp =  params_server->getParameter<double>("drone.pid." + axis + ".kp",0.0);

            double ki =params_server->getParameter<double>("drone.pid." + axis + ".ki",0.0);

            double kd = params_server->getParameter<double>("drone.pid." + axis + ".kd",0.0);

            std::cout  << axis << ": "   << kp << ", "  << ki << ", " << kd << std::endl;
        }

        std::cout << "==============" << std::endl;

        std::this_thread::sleep_for(
            std::chrono::milliseconds(200));
    }

    return 0;
}

2-3 单次访问测速
cpp 复制代码
#include "../include/test_pkg/ParamsServerUltra.hpp"
#include <iostream>
#include <chrono>

int main()
{
    auto* params =
        ParamsServerUltra::getInstance(
            "src/test_pkg/params/config.yaml");

    // 预热(避免第一次 load + page fault 干扰)
    for (int i = 0; i < 1000; i++)
    {
        params->getParameter<double>("drone.pid.x.kp",0.0);
    }

    const int N = 1000000;

    auto start = std::chrono::high_resolution_clock::now();

    double sum = 0.0;

    for (int i = 0; i < N; i++)
    {
        sum += params->getParameter<double>("drone.pid.x.kp",0.0);
    }

    auto end = std::chrono::high_resolution_clock::now();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);

    double avg_ns =duration.count() / (double)N;

    std::cout << "Total: " << duration.count()<< " ns\n";

    std::cout << "Average: "<< avg_ns << " ns/op\n";

    std::cout << "sum = " << sum << std::endl;

    return 0;
}
  • 也就是0.000788823 ms

总结

  • 本文通过对 YAML 多级结构进行扁平化设计,在保留可读性的同时,将参数访问统一收敛为 O(1) 哈希查询,从而实现了高性能、支持热更新的 C++ 参数服务器。
  • 如有错误,欢迎指出!
  • 感谢观看!
相关推荐
啦啦啦啦啦zzzz1 小时前
算法总结(双指针)
c++·算法·双指针
去码头整点薯条981 小时前
网络实验报告9
运维·服务器·网络
QiLinkOS2 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议
坚果派·白晓明2 小时前
鸿蒙PC】libuv适配:AtomCode Skills一站式指南
c语言·c++·华为·ai编程·harmonyos·atomcode
c++之路2 小时前
CMake 系列教程(五):进阶技巧
c语言·开发语言·c++
影寂ldy2 小时前
C# 三大内置委托(Action / Func / Predicate)+ Lambda
c++·算法·c#
A15362553 小时前
六轴工业机械臂厂家怎么选?评估维度与选型参考
大数据·服务器·人工智能
字节高级特工3 小时前
智能指针原理与使用场景全解析
开发语言·c++·算法
睡一觉就好了。3 小时前
make基础
linux