浅淡Apollo Cyber RT之数据缓存与融合
Cyber RT的层次图如下:
今天要讲的内容位于上图中的中间层。
一、基于Cyber RT的开发流程
在开发基于Cyber RT的模块时,并不是从main()函数开始的,常规的流程是这样的:
以PlanningComponent为例
● 从Component派生一个子类PlanningComponent。
● 重新实现虚函数Proc()和Init()。
Init(),组件初始化函数,当进程初始化时,被CyberRT框架调用,我们在此添加我们的初始化逻辑。
Proc(),数据处理函数,每当数据就绪时,被CyberRT框架调用,我们在此添加我们的业务处理逻辑。
这一切看起来很简单,Cyber RT将大量的工作都隐藏在这两个函数后面。
先看Proc()函数的声明:
可以看到,Planning组件依赖三组数据输入,分别是预测障碍物、底盘数据和定位信息。在没有使用Cyber RT之前,这些数据的对齐需要我们自己来做,其中包括时间对齐、线程同步和数据拷贝。这些工作很繁琐,且容易出错,且每个模块都要做大量这样的重复性工作。现在Cyber RT帮我们做了这些工作,我们只需要关注业务逻辑的处理。那Cyber RT是怎么做到的呢?
二、数据处理流程
这个一个精简的数据流图,图中只标出了核心的调用关系,但足以说明问题。主要包括以下步骤:
① Init()是被框架中的Initialize()函数调用,Initialize()函数还做了其它大量的工作。它通过CreateReader()创建了多个Reader,分别绑定以Proc函数中的多个消息类型。
图1.1
图1.2
图1.3
CreatReader()函数间接调用了ReceiverManager,向Transport层订阅消息。
② Initialize()函数紧接着创建了一个数据访问器(DataVisitor),DataVisitor的引用被保存在异步Task中。每个Component有专属的DataVisitor,DataVisitor又绑定了相关的一组消息。DataVisitor还做了以下几项工作:
2.1 为每个消息类型创建一个消息队列。
2.2 创建一个消息对齐/融合器(Fusion/Alllatest)。这一组消息对齐的频率,等于第一个消息M0的频率,所以只向M0的消息队列注册回调函数。
③ 通过CreateTask向调度器中注册回调任务(协程)(图1.3)。调度器会向DataVisitor注册自己(再通过Notify向DataNotify注册),这样,当Transport层数据就绪时,调度器就会收到通知。调度器再根据消息优先级,结合调度策略,唤醒协程,最终调用到组件的Proc函数。
上面介绍了组件(Component)的初始化过程。而一个完整的消息处理流程分为后面几个步骤:
④ 被动接收底层(Transport)的消息。
⑤ 收到消息后,没有直接发送到应用进行处理,而是发送到了数据分发器(DataDispatcher)。
⑥ DataDispatcher再将消息放到相关消息的缓存队列。
⑦ 其中Fill()函数中缓存数据,并执行前面 (2.2) 注册的对齐/融合回调函数。
⑧通过DataNotifier调用前面 (③) 注册的回调函数(RegisterNotifyCallback()),通知调度器(NotifyProcessor()),进行后续处理。
⑨ NotifyProcessor() 根据优先级和调度策略唤醒协程,Cyber实现了两种策略,分别是"SchedulerClassic"和"SchedulerChoreography"。这里不作详述,更多信息请参见:
https://zhuanlan.zhihu.com/p/121042548。
要注意的是,到目前为止的这一系列的调用,都是在Transport的回调函数(线程)中进行。而被调度器唤醒后的任务是在调度器框架中的协程中执行。这是一个异步的过程,它保证了上层应用处理,不会阻塞底层消息收发。
⑩ 协程处理函数是一个无限循环。
⑾ 协程函数首先(通过DataVisitor)向缓存中读取对齐后的数据。
⑿ 获得融合后的数据后,协程会调用应用组件(Component)的回调函数。
而这个回调函数就是我们在前面①中初始化组件时注册进来的
而Process()函数最终会调用我们重新实现的Proc()函数,到此,一个完整的数据处理流程完成!
附完整时序图:
总结,Cyber RT 数据处理最大特点是:
1、上层业务逻辑与底层数据收发解耦,包括静态(设计)和动态(运行)的。
2、数据对齐/融合。
3、协程调度器。