历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "核心状态与缓冲管理" description: >- workload() 聚合输入队列长度与算法内部负载;wait_idle 以轮询等待三个条件(无输入、无 processing、算法负载为 0);stop/save_map/process_once 构成算法状态管理的接口面。
核心状态与缓冲管理
本页聚焦三个问题:负载如何被量化(workload())、“排空”如何被等待(wait_idle())、算法内部状态如何被统一管理(stop / save_map / process_once 接口面)。这三者共同构成了“异步队列层 → 算法插件层”之间的状态契约,是理解停机时序与地图保存安全性的前置知识。
模块职责划分
状态与缓冲被明确切分为三层,各自拥有独立的保护机制:
| 层 | 文件 | 持有的状态 | 保护方式 |
|---|---|---|---|
| 缓冲调度层 | core/async_odometry_estimation.{hpp,cpp} |
IMU/帧两条环形缓冲(1000/50);running / end_of_sequence / processing 三个原子标志 |
mtx + cv 管队列,原子变量管生命周期 |
| 算法接口层 | core/odometry_estimation.hpp |
只定义契约,不持状态 | 虚函数默认实现全部为“空操作/零负载” |
| 算法实现层 | odometry/fastlio/odometry_estimation.cpp 等 |
各自的 lidar/imu 内部缓冲、地图状态、滤波器状态 | 各自的 buffer_mutex(以 FastLIO 为例) |
关键约束是:只有 worker 线程会调用算法的 insert_imu / insert_frame / process_once(见 loop()),算法内部估计状态因此在正常路径上是单线程的;但 save_map / stop 会被其他线程调用,所以 FastLIO 用 buffer_mutex 把这两者与 sync_packages 的出队操作互斥起来。
workload():两层负载的聚合
AsyncOdometryEstimation::workload() 的语义是一个加法聚合:
- 第二项来自基类契约
virtual int workload() const { return 0; },不报告内部负载的算法自动退化为“只看输入队列”。 - FastLIO 的实现返回
lidar_buffer.size()——注意它不含imu_buffer,即“算法负载”的口径由各实现自行决定。 - 上层
AsukaROS::workload()只是透传。
两个必须记住的边界:
workload() == 0不等于“计算已结束”。worker 在loop()中逐条取出事件、进入processing = true的排空段;此期间输入队列可能是空的,但process_scan仍在运行并写内部地图状态。save()处的注释明确指出这正是数据竞争来源。odometry->workload()在 async 的mtx持有下被调用。算法实现者必须保证自己的workload()轻量且不可重入回 async 层,否则会自锁。
flowchart LR
subgraph ROS层["AsukaROS (生产者/上层)"]
P["insert_imu / insert_frame<br/>(标定补偿后投递)"]
W1["wait(auto_quit): 轮询 workload()==0"]
end
subgraph 缓冲层["AsyncOdometryEstimation"]
Q["imu_queue / frame_queue (1000/50)"]
FL["running / end_of_sequence / processing (atomic)"]
WL["workload() = 双缓冲长度 + odometry->workload()"]
end
subgraph 算法层["OdometryEstimation 插件 (如 FastLIO)"]
IB["lidar_buffer / time_buffer / imu_buffer<br/>(buffer_mutex)"]
MC["内部地图状态 (process_scan 写入)"]
end
P -->|"mtx+cv"| Q
WK["worker 线程 loop()"] -->|"逐条取出"| Q
WK -->|"发射输入槽 → insert_* → while(process_once()) 排空"| IB
IB -->|"sync_packages → process_scan"| MC
WL -.聚合.- Q
WL -.聚合.- IB
图中要点:workload() 是跨越缓冲层与算法层的聚合视图;processing 标志恰好覆盖“队列已空但计算未完”的窗口,是 workload 口径的补丁。
wait_idle():三条件轮询排空
wait_idle()(async_odometry_estimation.cpp L40-50)以 10ms 轮询等待三个条件同时成立,是全链路“真正空闲”的强判据:
flowchart TD
A["wait_idle() 进入"] --> B["加锁读取:<br/>输入队列是否为空"]
B -->|"仍有输入 pending"| S["sleep 10ms"] --> B
B -->|"无输入"| C{"processing == true ?"}
C -->|"worker 正在喂入/排空"| S
C -->|"否"| D{"odometry->workload() > 0 ?"}
D -->|"算法内部仍有积压"| S
D -->|"== 0"| E["break: 全链路已排空"]
- 三个条件分别对应三层状态:输入队列空(mtx 保护)、
processing == false(原子读,锁外)、算法负载为 0(算法自报)。缺一不可,这正好补上了workload()==0的语义漏洞。 - 函数签名为
const,靠mutable std::mutex(hpp L35)实现“只读等待”,允许上层在 const 上下文中阻塞等待。 - 与之对照,
AsukaROS::wait(auto_quit)(asuka_ros.cpp L110-116)只以 100Hz 轮询async->workload() == 0,不检查processing——这是弱判据,主入口依赖后续save()中的stop()join 来兜底。需要精确排空的调用方(如离线回放等待收尾)应使用wait_idle而非wait。
stop 与入队拒绝:缓冲层生命终点
stop() 通过 end_of_sequence = true + running.exchange(false) 实现幂等:重复调用、或析构函数再次调用都安全。end_of_sequence 与 !running 一旦置位,insert_imu / insert_frame 直接丢弃新数据,保证 join 之后不会再有生产者写入。另一个隐含边界:两条输入缓冲是有界环形缓冲(1000/50),满员时静默覆盖该流最旧事件,生产速率超过消费速率时不会阻塞 ROS 回调,而是丢历史。
算法状态管理接口面
基类为状态管理定义了三个可覆盖的钩子,默认全为空操作:
| 接口 | 默认 | FastLIO 示范 | 调用方 |
|---|---|---|---|
stop() |
空 | 清空三个内部缓冲并复位 lidar_pushed,即 stop 兼具 flush 语义 |
析构链路(异步执行器停机另有自己的 stop()) |
save_map() |
返回 nullptr |
在 buffer_mutex 下 flatten ikd-tree 现场导出(受 mapping.enable_map_saving 门控) |
AsukaROS::save |
process_once() |
返回 false |
sync_packages 失败返回 false,成功则 process_scan 并返回 true |
worker 排空循环 |
process_once() 是泵式接口:worker 用 while (odometry->process_once()) {} 循环调用直到返回 false,算法用它表达“还有完整量测组可处理”。FastLIO 的 sync_packages 展示了典型协议——帧与 IMU 按时间戳对齐成 ImuMeasureGroup,不足则返回 false 等待下一轮。
一个必须理解的线程安全细节:save_map() 返回的是地图的共享指针句柄(fastlio 为 ikd-tree 的现场导出副本,batchlio/superlio 为运行期累积缓冲);如果调用方不先 stop() join worker,process_scan 仍可能在读句柄的同时写地图——这正是 save() 注释中“data race / heap corruption”警告的机制。算法状态的正确读出协议是 stop → join → save_map。
边界条件小结
workload()==0≠ 计算完成:需要processing补判(wait_idle已包含,wait未包含)。- 算法
workload()口径自定(FastLIO 不含imu_buffer),不同实现的“空闲”含义不完全一致。 - 环形缓冲满即覆盖该流最旧数据,无告警、无阻塞。
stop()后入队被静默丢弃;wait_idle在 stop 之后会立即返回(三条件天然满足)。workload()在 async 互斥锁内调用算法实现——实现不得阻塞或回调 async 层。save_map()能力因算法而异:fastlio 现场导出(受mapping.enable_map_saving门控)、batchlio/superlio 返回累积缓冲、lightning 导出全局地图、smallpointlio 返回空。
扩展点
- 新算法接入:务必诚实实现
workload(),否则wait_idle对该算法退化为“只等输入排空”,可能在其内部仍有积压时提前返回;save_map()也应返回真实地图而非 nullptr(或明确以 nullptr 表达"不产出地图")。 - 等待机制升级:
wait_idle目前是 10ms 轮询而非条件变量唤醒,若将来有低延迟需求,可在processing置 false 处增加cv.notify改造为事件驱动,轮询结构本身即为接缝。
Sources
Sources: src/asuka/src/asuka/core/async_odometry_estimation.cpp#L1-L86
Sources: src/asuka/include/asuka/core/async_odometry_estimation.hpp#L1-L45
Sources: src/asuka/include/asuka/core/odometry_estimation.hpp#L1-L40
Sources: src/asuka/src/asuka/asuka_ros.cpp#L1-L186
Sources: src/asuka/src/asuka/odometry/fastlio/odometry_estimation.cpp#L1-L260
关联源码
src/asuka/include/asuka/core/odometry_estimation.hppsrc/asuka/src/asuka/core/async_odometry_estimation.cpp