
1. 拷贝每帧 10MB 带宽先爆
相机 30Hz、每帧 8–12MB raw,pipeline 三四个 stage 若按值拷贝,memcpy 占满一个核——算法还没跑。语义上帧也只有一个「当前主人」:decode 交给 filter,filter 交给 publish,中间不应隐式共享。只移动队列用 delete copy + move pop 把约束写进类型;比注释「别拷贝」可靠,也比默认 shared_ptr 更贴单路径 ownership。无界 queue 下 move 也救不了 OOM kill——深度预算与 move 语义同样重要。profiling 若显示 memcpy 顶满而算法很轻,先查 queue 接口是否允许 copy,再查 depth 是否无界堆满内存。多 stage 线程化时,queue 是 ownership 边界:producer push move 进队,consumer pop move 出队,中间不应出现「为了线程安全而 copy 一份」的偷懒实现。
2. move-only Frame
Frame 删 copy、默认 move;多消费者看同一帧才 shared_ptr<const Frame>。move ctor 标 noexcept,否则 vector<Frame> 扩容会 silent copy 大 buffer。类型名表达语义:能 copy 的 Frame 会在 review 里被 push_back(frame) 漏 move——delete copy 让错误编译期暴露。Frame 内部大 buffer 用 unique_ptr 或 vector 持有,move 只转移指针,保证 queue 里 push/pop 是 O(1) 所有权转移而非 memcpy。
struct Frame {
std::vector<uint8_t> raw;
Frame(const Frame&) = delete;
Frame& operator=(const Frame&) = delete;
Frame(Frame&&) noexcept = default;
Frame& operator=(Frame&&) noexcept = default;
};3. mutex + deque 有界队列
push 超 max_depth 拒收或 drop,try_pop 转移所有权。deque pop front O(1);vector 会 memmove 整段 buffer。别 peek 返回引用——预览另用 shared_ptr<const>。FrameQueue 本身也 delete copy,防误拷贝整个 pending 队列进日志或诊断路径。drop 策略写进 README:拒收新帧 vs drop oldest,与产品语义一致;Throttled 日志带 stamp,便于和录包对齐复盘哪段 burst 填满了队列。
std::optional<Frame> try_pop() {
std::lock_guard lk(mu_);
if (q_.empty()) return std::nullopt;
Frame f = std::move(q_.front());
q_.pop_front();
return f;
}4. SPSC 与 unique_ptr
单生产者单消费者可用 moodycamel 无锁 ring;try_enqueue 失败即 drop 并计数,别 silent 阻塞 producer。多消费者仍要 mutex 或 per-consumer 队列。boost::lockfree 要求 trivially copyable,move-only 不适用——选型前先读库约束,别到链接期才发现。SPSC 路径可把 Frame 包在 unique_ptr 里进 ring,consumer pop 后 unique_ptr 独占一帧,避免 queue 内大 struct move 的开销若 Frame 本身已轻量则直接 move Frame。无锁队列不是免费午餐:capacity 固定、满则 drop、内存序写错难 debug——先证明 mutex 瓶颈在 profile 里,再换 SPSC ring。
5. condition_variable 与 shutdown
wait_and_pop 带 predicate:!q_.empty() || !running_。shutdown 设 running_=false 再 notify_all,否则 join 永久阻塞——比 leak 更难查。notify 前完成 push,锁内 move 出队。drop 策略写进 README:拒收新帧 vs drop oldest,与产品语义一致。on_shutdown 顺序:running_=false → notify_all → join workers → 清空 queue,别先 destroy subscriber 后 worker 仍 pop。
6. 深度与内存预算
深度 1024 × 平均 2MB ≈ 2GB 峰值——监控 size()>0.8*max 打 WARN,主动 drop。queue depth 写进运维面板:峰值内存 = 深度 × 平均帧大小,别只盯 CPU。Throttled 日志带 stamp,便于和录包对齐复盘哪段 burst 填满了队列。产品 spec 应写清「满队列时丢新还是丢旧」,实现与 spec 不一致时现场表现为延迟突增或画面卡顿,难与算法 bug 区分。
7. 案例:shutdown 卡死
节点 Ctrl-C 后进程不退出——worker 在 wait_and_pop 永久等,shutdown 忘了 notify_all。补 running_ 标志与 predicate 后,join 两秒内返回。这类 bug move 语义救不了,但只移动队列往往配 blocking pop,shutdown 契约必须写进类文档与 on_shutdown 顺序。CI 可加 shutdown 集成测:起节点 → 灌几帧 → rclcpp::shutdown → 断言进程 exit code 0 且超时内返回。
8. 验收
benchmark:copy vs move-only pipeline,同 bag 可区分。ASan:moved-from 不二次访问。shutdown:stop → notify_all → join 超时内返回。review:未见 vector<Frame> 全拷贝;push 对局部 frame 有 move。压测 burst 时 drop 计数与产品 spec 一致。queue depth 曲线进运维面板,与录包时间轴对齐可查 burst。latency 指标应区分「queue 等待时间」与「算法处理时间」——前者满队列时飙升,后者才反映算力,混在一个 timer 里会误调算法。
9. 与录包、运维对齐
事故袋若 queue depth 曲线缺失,只能猜是算法慢还是队列堆满。只移动队列表达「帧只有一个主人」——pipeline ownership 图里共享点应屈指可数且文档化。录包应能还原 drop 计数与 max_depth 配置,否则复盘「丢帧」分不清是算法 reject 还是 queue 满。移动语义与有界深度是同一问题的两面:前者省带宽,后者省内存,缺任一都会在 field 里爆。设计评审时画三条线:producer 速率、consumer 处理能力、queue max_depth——任意两条长期不匹配,第三条只能体现为 drop 或延迟,别指望「队列加深」无限缓冲。
相关
也可以看看
johan's blog