时间、实时性与确定性
理解周期、抖动、截止时间和动态分配对机器人控制循环的影响。
时间、实时性与确定性
Web 服务常看平均响应时间,机器人控制更关心每一次周期是否在截止时间前完成。动态分配、mutex 等待、日志、系统调用和不可预测的网络都会制造抖动。实时性不是把 setInterval 翻译成 sleep,而是定义周期、截止时间、允许的最大延迟、过期数据处理和安全降级。
周期、截止时间和抖动要分开
setInterval(() => {
updateControl();
}, 10); using Clock = std::chrono::steady_clock;
auto next = Clock::now();
while (running) {
next += 10ms;
update_control();
std::this_thread::sleep_until(next);
} 周期是理想启动间隔,执行耗时是从开始到结束,截止时间是允许的完成时刻,抖动是实际时间相对计划时间的波动。若每次都从“当前时间”再加 10ms,处理超时会让循环漂移;按绝对 next 调度可以暴露漏周期。steady_clock 不受系统时间校准跳变影响,适合测时长。
一个可观测的控制循环
#include <chrono>
#include <thread>
void control_loop(std::stop_token stop) {
using Clock = std::chrono::steady_clock;
constexpr auto period = std::chrono::milliseconds{10};
auto deadline = Clock::now() + period;
while (!stop.stop_requested()) {
const auto started = Clock::now();
update_control();
const auto finished = Clock::now();
if (finished > deadline) record_missed_deadline(finished - deadline);
std::this_thread::sleep_until(deadline);
deadline += period;
(void)started;
}
}
示例需要补上工程自己的统计实现;真正实时线程还要配置调度优先级、内存锁定和 CPU 隔离,不能仅依赖循环代码。控制路径预分配消息、避免无限阻塞,数据过期时进入明确安全状态,例如停止速度命令,而不是继续使用旧目标。
确定性来自资源纪律
平均耗时低不代表最坏耗时低。为周期建立内存预算,避免在回调内第一次触发 allocator、动态加载或日志格式化;把慢操作移到非实时线程,通过有界队列交接。锁可以造成优先级反转,网络可以产生不可控重传,因此实时线程应尽量拥有局部状态和无阻塞输入。
常见编译和运行时排错
使用 system_clock 测间隔会遇到时间跳变;使用 sleep_for 会积累漂移;把 std::cout 放进高频循环会制造 I/O 抖动。p50 正常但 p99 很差时,查看调度、分配、锁和 page fault。Missed deadline 不一定是算法慢,也可能是线程没有被调度;记录开始/完成时间、CPU、队列深度和输入年龄再判断。
学习目标
完成本节后,你能区分周期、执行时长、deadline 和 jitter;使用 steady_clock 的绝对时间点调度;统计 missed deadline、输入年龄和 p99;解释分配、锁、日志、网络和 page fault 对确定性的影响;为超时输入定义安全输出。
输入年龄与安全命令
struct Command { std::uint64_t stamp_ns{}; double velocity{}; };
Command validate(Command command, std::uint64_t now_ns) {
constexpr std::uint64_t max_age_ns = 50'000'000;
if (now_ns - command.stamp_ns > max_age_ns) return {now_ns, 0.0};
return command;
}
用固定的 10ms、50ms、51ms 输入年龄测试边界。0.0 只是示例安全动作,真实底盘可能要求减速、急停或保持姿态;动作必须由机器人安全设计定义,不能从教程示例直接照搬。
绝对 deadline 的三次循环
using Clock = std::chrono::steady_clock;
const auto period = std::chrono::milliseconds{10};
auto deadline = Clock::now() + period;
for (int cycle = 0; cycle < 3; ++cycle) {
update_control();
if (Clock::now() > deadline) record_miss();
std::this_thread::sleep_until(deadline);
deadline += period;
}
编译用 g++ -std=c++20 -pthread realtime.cpp -o realtime。按绝对 deadline 递增可以暴露漏周期;每次从 now 加 period 会在一次慢执行后永久漂移。验证时故意让 update_control 耗时 15ms,观察 miss 计数而不是延长 deadline。
JS/TS 迁移的实时反例
setInterval、setTimeout 和 sleep_for 都没有硬实时保证。不要在控制循环中做无界队列等待、动态分配、网络重试或格式化日志;把这些操作移到非实时线程,经有界消息交接。控制线程要有预算、过期数据规则和最大等待时间。
机器人实时性验收
每周期记录 cycle_index、started、finished、planned deadline、input_age、queue_depth 和 allocation count,统计 miss_count、max_lateness、p50/p99。p50 正常但 p99 很差时,分别排查调度、锁、分配和 page fault;没有这些字段,无法知道是算法慢还是线程没被调度。
迁移练习
设计 10ms 控制循环的测量表:每次记录开始、完成、计划 deadline、输入时间戳、队列深度和分配次数,统计 miss count、最大迟到、p50/p99。模拟处理突然耗时 15ms,说明下一周期是追赶、跳过还是进入安全模式。
写出你的实时性验收条件
为控制循环设定周期、截止时间和最大允许输入年龄;实现 missed deadline 与最大抖动统计,并说明超时后的安全动作。
给我一点提示
使用 steady_clock 和绝对 deadline;不要用 sleep 让超时消失,先记录它。
查看参考答案
用 steady_clock 计算 finished - deadline,增加 miss_count 并更新最大迟到;同时计算 now - message.stamp 判断数据年龄。若超过阈值,发布零速度或安全控制输出并记录原因;是否跳过一个周期要由控制器的稳定性验证决定。 本节结论
当时间预算、资源预算和超时动作都能被测量,实时性才从口号变成工程契约。下一节会把这种契约延伸到网络、IPC 和进程故障边界。
延伸阅读
先完成本节练习,再用这些资料查阅完整 API 和真实项目组织方式。
阶段共 7 节课,按顺序完成更容易建立完整的迁移模型。