本文档深入解析推理节点的线程层级、调度策略及跨线程同步机制。系统采用推理-控制双主线程架构:推理线程以较低频率执行 ONNX 模型推理与观测组装,控制线程以更高频率将推理结果平滑下发至电机硬件。该设计在确保策略网络有足够计算裕量的同时,维持了电机控制环路的高频响应。此外,底层电机通信通过自定义线程池按总线维度并行化,以最大化 CAN/CANFD 总线吞吐。
Sources: inference_node.hpp, inference_node.cpp
双线程架构总览
推理节点在运行时存在三条核心执行线:ROS 2 MultiThreadedExecutor 承载的主线程/回调线程、推理线程 (inference) 与 控制线程 (control)。主线程负责参数加载、服务响应与话题回调;推理线程负责观测堆叠、归一化与模型前向;控制线程负责动作插值与电机指令下发。三者通过原子变量与互斥锁共享状态,形成典型的“生产者-消费者”时序解耦模型。
flowchart TB
subgraph Main["主线程 / ROS Executor (SCHED_FIFO, prio 50)"]
A[参数加载] --> B[话题/服务回调]
B --> C[更新 cmd_vel<br/>perception_obs<br/>interrupt_action]
end
subgraph Inf["推理线程 inference (SCHED_FIFO, prio 70)"]
D[采集传感器] --> E[组装观测值<br/>帧堆叠与归一化]
E --> F[ONNX Runtime<br/>模型推理]
F --> G[写入 act_<br/>publish_action]
end
subgraph Ctrl["控制线程 control (SCHED_FIFO, prio 70)"]
H[读取 act_ / last_act_] --> I[动作平滑<br/>act_alpha 插值]
I --> J[robot_->apply_action<br/>下发至电机]
end
C -.->|cmd_mutex_<br/>perception_mutex_| D
G -.->|act_mutex_| H
J -.->|joint_mutex_<br/>motors_mutex| K[ThreadPool<br/>按总线并行]
Sources: inference_node.cpp, inference_node.hpp
推理线程:观测组装与模型前向
推理线程由 InferenceNode 构造函数在初始化阶段通过 std::thread 启动,线程函数为 InferenceNode::inference()。该线程被显式命名为 "inference",并设置为 SCHED_FIFO 实时调度策略,静态优先级 70。其循环周期由配置参数 dt 与 decimation 共同决定,计算公式为 period = dt * decimation。以默认配置为例,当 dt = 0.004、 decimation = 5 时,推理周期为 20 ms(50 Hz)。
每次循环中,推理线程首先通过 mode_mutex_ 锁定当前活跃策略,随后依次调用 update_obs_segments 采集 IMU、关节位置/速度、指令速度等观测源,并执行 flatten_obs_segments 与 update_stacked_obs 完成帧堆叠。观测经限幅后送入 ONNX Runtime 的 Session::Run,输出动作先经 clip_actions_ 限幅,再乘以 action_scale_ 并叠加 joint_default_angle_ 偏置,最终写入共享缓冲区 act_。整个写操作受 act_mutex_ 保护。若循环耗时超过设定周期,系统会输出 RCLCPP_WARN overrun 告警,提示推理流水线存在实时性风险。
Sources: inference_node.cpp, inference_node.hpp
控制线程:高频动作下发与平滑
控制线程同样以 SCHED_FIFO、优先级 70 运行,线程函数为 InferenceNode::control(),但其周期固定为 dt(不乘 decimation)。沿用上述默认配置,控制线程以 4 ms(250 Hz) 循环执行,远高于推理频率。这一设计的物理意义在于:电机力矩/位置伺服需要高频指令以维持稳定性,而神经网络推理的算力开销不允许同频执行。
控制线程的核心逻辑集中在 apply_action() 中。每次循环,线程先以 act_mutex_ 锁定 act_ 与 last_act_,执行一阶指数平滑:
last_act_[i] = act_alpha_ * act_[i] + (1 - act_alpha_) * last_act_[i]
其中 act_alpha_ 为平滑系数(默认配置中为 1.0,表示不插值、直接采用最新推理动作)。平滑后的 last_act_ 被传递至 RobotInterface::apply_action(),触发底层电机通信。由于 act_ 的更新频率仅为控制线程的 1/decimation,act_alpha_ 提供了在两次推理之间对电机指令进行渐变过渡的能力,避免动作跳变。
Sources: inference_node.cpp, inference_node.cpp
线程间同步与数据流
双主线程与 ROS 回调之间存在多条共享数据通道,系统通过细粒度互斥锁避免伪共享与优先级反转。下表汇总了各锁的防护范围与访问线程:
| 互斥锁 | 保护数据 | 写方 | 读方 |
|---|---|---|---|
act_mutex_ |
act_, last_act_ |
推理线程 | 控制线程 |
mode_mutex_ |
active_policy_idx_, 策略运行时状态 |
回调线程(LB 切换) | 推理线程 |
cmd_mutex_ |
cmd_vel_ |
手柄/cmd_vel 回调 |
推理线程(观测组装) |
perception_mutex_ |
perception_obs_buffer_ |
感知话题回调 | 推理线程 |
interrupt_mutex_ |
interrupt_action_ |
中断关节状态回调 | 推理线程 |
lb_switch_mutex_ |
策略切换原子序列 | 手柄回调 | — |
此外,is_running_、is_interrupt_、is_motion_policy_ 等状态量采用 std::atomic<bool>,实现无锁的跨线程标志位读取。推理线程在 is_running_ 为 false 时直接睡眠整个周期,避免空转占用 CPU。
Sources: inference_node.hpp
电机通信并行化:ThreadPool
RobotInterface 内部持有一个自定义 ThreadPool,其工作线程数量等于系统配置的电机总线数(motors_cfg_->motor_interface_.size())。每个工作线程同样被设置为 SCHED_FIFO、优先级 70,线程名统一为 "threadpool"。该线程池的职责是将跨多条 CAN/CANFD 总线的电机指令并行下发,最大化硬件 I/O 吞吐。
在 RobotInterface::apply_action() 中,若接口类型为 canfd,系统按总线将电机拆分为多个批次,每个批次打包为一条 std::function 任务,通过 thread_pool_->run_parallel(tasks) 并发执行。run_parallel 内部先将任务 enqueue 到队列,再依次 wait + get 所有 std::future,实现同步屏障。对于非 CANFD 接口,则通过 exec_motors_parallel 以同样逻辑并行遍历各总线上的电机实例。joint_mutex_ 与 motors_mutex_ 分别保护传感器回读数据与电机目标值,避免状态竞争。
Sources: thread_pool.hpp, robot_interface.cpp, robot_interface.cpp
实时性保障机制
系统从内核调度、内存锁定与推理运行时三个层面构建了实时保障体系:
1. 实时调度策略
主线程、inference、control 及线程池工作线程均显式请求 SCHED_FIFO。优先级分配遵循“越快越近硬件,优先级越高”的原则:主线程为 50,推理/控制/线程池为 70。该分层确保即使 ROS 回调突发,也不会抢占电机指令下发或模型推理。
2. 内存锁定
main() 入口调用 mlockall(MCL_CURRENT | MCL_FUTURE),将当前及未来分配的全部虚拟内存锁定在物理 RAM 中,避免运行时因缺页中断(Page Fault)引入不可预测延迟。若调用失败,系统仅输出 WARN,不会终止进程。
3. ONNX Runtime 线程隔离
setup_model() 中为每个 Ort::SessionOptions 调用 DisablePerSessionThreads(),禁止 ONNX Runtime 创建内部线程池;同时通过 Ort::ThreadingOptions 将全局 intra-op 线程数限制为 intra_threads_(默认 1)。该配置消除了 ONNX 内部线程与系统自定义 ThreadPool 之间的 CPU 争抢,确保推理耗时可控。
4. 故障快速熔断
推理线程与控制线程均在 try-catch 块内执行核心逻辑。一旦发生异常,立即输出 RCLCPP_FATAL 并调用 rclcpp::shutdown(),强制退出进程。该策略符合实时机器人系统的“故障即停”安全哲学,避免错误动作持续输出。
Sources: inference_node.cpp, inference_node.cpp, inference_node.cpp, thread_pool.hpp
调度参数与运行时配置
下表列出与双线程调度直接相关的关键参数及其典型取值:
| 参数 | 类型 | 含义 | 典型值 |
|---|---|---|---|
dt |
float | 控制线程周期(秒) | 0.004 (250 Hz) |
decimation |
int | 推理相对于控制的降采样倍数 | 5(推理 50 Hz) |
act_alpha |
float | 动作平滑系数 | 1.0(无平滑) |
intra_threads |
int | ONNX intra-op 并行线程数 | 1 |
clip_actions |
float | 输出动作限幅值 | 100.0 |
clip_observations |
float | 观测值限幅值 | 100.0 |
dt 与 decimation 共同决定了系统的“控制-推理”时序配比。降低 dt 可提升电机响应,但会增大控制线程 CPU 占用;增大 decimation 可为复杂模型争取更多推理时间,但会降低策略对环境变化的反应频率。实际部署中需结合模型延迟与电机动力学进行联合调优。
Sources: config/inference.yaml, inference_node.cpp
与相关文档的关联
理解双线程设计后,建议继续阅读以下页面以建立完整运行时认知:
- 系统架构总览 — 从全局视角理解推理节点在软件栈中的位置
- 多策略运行时与热切换机制 — 深入
mode_mutex_保护下的策略切换与中断机制 - ONNX Runtime 推理引擎集成 — 了解
DisablePerSessionThreads与内存池配置细节 - 观测值组装、归一化与帧堆叠 — 理解推理线程中观测数据的完整处理流水线
- RobotInterface 设计与实现 — 掌握
ThreadPool与exec_motors_parallel的硬件抽象细节 - 性能调优与实时性保障 — 获取内核参数、IRQ affinity 与 CPU 隔离的进阶调优方法