
1. 均值正常、p99 偶发 200ms
相机 30Hz、IMU 200Hz、lidar 10Hz 进同一融合节点——一个大 mutex 包全部 callback,lidar 延迟均值正常、p99 偶发 200ms。典型做法是 IO/解码、融合估计、控制发布三条线程加有界 SPSC 队列,背压策略与优先级一起设计。传感器并发不是「加线程就快」——选错模型表现为 jitter 飙、偶发丢帧、或 priority inversion 拖死控制环,CPU 不高也可能丢帧,因为队列满且策略是 drop new 而非 old。均值正常掩盖 p99,field 才暴雷;diagnostics 必须能看见 drop 与 queue depth;cyclictest 与 TSan 是合入门槛,不是 field 才补的保险。录 bag 与实时导航的满队列策略不同,同一队列别隐式混两种语义。
2. 线程划分
IO/解码:blocking read 加 parse,输出固定 struct;IRQ 线程禁止 malloc——预分配 ring buffer。融合/估计:单线程消费 synchronized 数据,避免锁中间状态。控制/发布:高优先级,只读 atomic 最新状态 snapshot。ROS 2 里也可单 executor 多 callback group,但重 decode 仍建议 worker thread 加 SPSC 喂 executor。跨线程只 move unique_ptr<Msg>,禁止共享 mutable PointCloud。Waitable 把 poll fd 并进 executor 可省线程,但 decode 仍要 bounded。三线程模型的契约是:谁拥有 buffer 可变权、谁在哪个优先级、满队列时丢谁——写进 README 与 diagnostics,别靠口头约定。
3. SPSC 与背压
bool SpscQueue<T, N>::try_push(T item) {
const auto w = write_.load(std::memory_order_relaxed);
const auto next = (w + 1) % N;
if (next == read_.load(std::memory_order_acquire)) return false;
buf_[w] = std::move(item);
write_.store(next, std::memory_order_release);
return true;
}队列 depth 有限:传感器侧丢旧保新,录 bag 可 block。相机 30Hz、SLAM 15Hz 时 unbounded queue 必爆内存。try_push 失败计数 export 到 diagnostics,别 silent drop。MPSC 别手写 unless 有 paper 级把握——用已验证组件,先把语义写对再谈自研无锁。ring 满策略写 README,drop 计数进 diagnostics;录 bag 与实时导航的满队列策略往往不同,同一队列别隐式混两种语义。
4. 双缓冲与序号
双缓冲 swap 指针时,写侧完成 payload 后对 active_idx release store,读侧 acquire load 再读 buffer——与 atomic 序号模式同构,别用 plain int 索引。head/tail 常分别由两端线程写,slot payload 在 index 发布前写完,否则读者看到 torn write。SPSC 队列深度要在 README 写清:为何选这个 depth、满时丢旧还是丢新、drop 计数如何 export——否则 field 只能看到「偶发跳帧」却定位不到策略。
5. 实时优先级与亲和性
SCHED_FIFO 给 decode/控制前,确认它拿的锁不会被低优先级线程长时间占——mutex 持有过长会把整链饿死,priority inversion 不是理论。短临界区加无锁 ring 更常见;RT 线程里别 malloc 或拿 mutex。mlockall 可选,先测 max latency。decode affinity 绑 isolated core 后 UDP 丢包率可降——与 IRQ affinity 冲突时用 /proc/interrupts 查 smp_affinity。NUMA 上 decode 与 fusion 绑不同 socket 可能反而慢,先 top -H 找 bottleneck。Orin 上 GPU preprocess 与 CPU tracking 抢核,affinity 要实测,别照搬 x86 lab 结论。
6. lidar 三线程模型
lidar UDP 收包在 driver 线程,decode 在 jthread,ROS publish 在 MultiThreadedExecutor。跨线程只 move unique_ptr<Msg>。IRQ 禁止 malloc——预分配 ring。SCHED_FIFO 仅给 decode,executor 保持 SCHED_OTHER,别全家 RT。TSan 加一小时 bag replay 是合入门槛。decode 绑 isolated core 后 UDP 丢包率可降——与 IRQ affinity 冲突时用 interrupts 查 smp_affinity,field 上零点几 percent 丢包在 long run 就是地图缺口。
7. 案例:无界队列拖垮内存
某融合节点 unbounded queue 接相机,SLAM 15Hz 跟不上 30Hz,两小时 field 后 OOM——均值延迟仍「正常」。改成有界 SPSC 丢旧保新并 export drop 计数后,内存平稳,p99 可解释。根因不是 SLAM 慢,是背压契约缺失。教训:depth、满策略、drop 计数三件套写进 README 与 diagnostics。录 bag 与实时导航的满队列策略往往不同——前者 block、后者 drop,别一套代码两种语义却不分支。
8. 验收
- ros2 topic hz 对比 sensor 与 fusion 输出。
- TSan 加一小时 bag replay。
- ring 满策略与 drop 计数可观测。
- cyclictest 测 RT max latency。
- affinity 与 IRQ 冲突时用 interrupts 查 smp_affinity。
背压加优先级加队列深度——缺一项,jitter 和丢帧会在 field 冒出来,均值却可能一直正常;diagnostics 里看不见 drop,就是在 silent 丢数据。传感器线程模型应在设计文档里写清:满队列丢谁、RT 绑哪条线程、与 executor 如何交接;合入前 TSan 加一小时 bag replay 是硬门槛,cyclictest 测 RT max latency 不可省略。
相关
也可以看看
johan's blog