Skeleton Class
代码生成和构造
skeleton class也是直接从ARXML生成的。
开发人员要继承这个虚类,然后在子类里实现 架构设计的 本服务要提供的方法。
skeleton实例的构造函数重载:可以从
ara::com::InstanceIdentifier
、ara::com::InstanceIdentifierContainer
(multi-binding)、
ara::core::InstanceSpecifier
构造。
void OfferService();
void StopOfferService();
都是幂等(idempotent)函数。随便调用多少次都和调用一次效果相同。
从同一个skeleton class可以部署很多的服务实例。当然了每次构造都要基于不同的identifier。在构造前会有一个静态类成员方法来检测该identifier是不是独一无二的。如果基于同一个identifier构建新实例,之前已经构造好的需要先销毁。这样也可以理解为什么不让使用复制构造函数和复制分配(copy construction nor copy assignment)。同样的identifier会让method调用之类的变得不确定。
下面来看三种不同的构造输入:
- ara::com::InstanceIdentifier
依据具体的binding描述符创建服务
- ara::com::InstanceIdentifierContainer
multi-binding…依据容器包含的多个描述符创建实例。多绑定
-
ara::core::InstanceSpecifier
依据 在service manifest中查找ara::core::InstanceSpecifier获得的 instance identifier(s)来创建服务实例,如果在manifest中这个InstanceSpecifier是map到了多个binding的描述符,那么就是multi-bingding.
在构造实例后,该实例会对用户不可见,以防止潜在的用户调用它的method。直到在服务的程序中调用
OfferService API
。
Offering Service instance
在初始化服务提供实例,并确保state之类的都到位之后,调用OfferService()
来提供服务。
调用OfferService()
,哪怕还没返回,service就已经开始接收服务请求了。
如果不想提供服务了,调用StopOfferService()
。该API返回之后不再接收服务请求。
AP要求供应商在服务实例的析构函数中嵌入StopOfferService()。
轮询和事件驱动模式
服务调用何时发生是不可确定的。
对纯粹的事件驱动来讲,CM要生成events calls。然后把这些events转换成具体的method调用,再调用具体的service实现的service methods。
这样的结果是:
- 直接调用服务Method是最快的,延时只受到整体的负载和IPC机制的限制。
- 包含服务实例的OS进程上下文切换可能会很频繁且不确定。降低总的产出。
我们还支持纯纯的轮询方式。服务提供者可以显示调用ara::com的API来执行单个 call event。
构造函数的第二个参数是描述生成实例的处理方式(轮询还是事件之类的)
enum class MethodCallProcessingMode { kPoll, kEvent, kEventSingleThread };
在服务实例的整个生命周期内,该服务实例都是同一种mode。
Polling Mode
设为kPoll
,CM不会异步调用提供的服务method。
在服务实例的代码中显式调用:
ara::core::Future<bool> ProcessNextMethodCall();
可以从CM的待处理Method call队列中取一条来执行。本API仅限Poll模式调用。
用Future的好处是在执行完之后可以得到提醒。可以使用future里的then(), 来判断在条件成熟的情况下,再调用ProcessNextMethodCall(),实现链式调用。
在有外部调用的时候,ara::core::Future的返回值会被CM设置为true。方便开发人员在队列里有调用请求排队的时候调用ProcessNextMethodCall。如果上一个调用已经返回false了,再调用可能不会做任何事情。(也有可能在返回的这一点点时间里来了一个调用,可能性很小)。
Event-Driven Mode
设为kEvent
或者kEventSingleThread
。CM就会在收到服务消费者请求的时候异步的调用服务实例的Method。
与轮询模式不同,服务用户会在调用method的时候触发服务提供者的活性(可以唤醒?是这个意思吧)。
KEvent
让CM可以并发调用服务Method。比如有来自不同服务用户的一个MethodA和两个MethodB同时到达,CM会从线程池里拉3个线程来并发处理这些Method调用请求。
kEventSingleThread
就是CM一次调用一个service method。CM需要维护一个队列来缓冲请求,然后一个个调用method处理。
设计两种方式是为了效率和节省资源考虑。如果Method调用间有大量的数据同步需求(比如先后依赖),建议使用kEventSingleThread
避免大量空闲线程等待关键线程数据结果。
Methods
skeleton 生成类中。Method定义为纯虚函数。必须在子类中复写。
输入参数就直接映射到了Method的输入参数。返回值使用了ara::core::Future。
为什么要用Future呢,是因为我们会使用线程池来处理做Method的工作,调用的Method入口函数需要与线程池真正干活的线程同步(就是要等他干完活才能返回)。
面对多线程编程,和传统嵌入式的编程方式真的是有些不一样啊
下面来看两种实现方式 “同步”和“异步”:
using namespace ara::com;
/**
* Our implementation of RadarService
*/
class RadarServiceImpl : public RadarServiceSkeleton {
public:
Future<AdjustOutput> Adjust(const Position& position)
{
ara::core::Promise<AdjustOutput> promise;
// calling synchronous internal adjust function, which delivers
results
struct AdjustOutput out = doAdjustInternal(
position,
&out.effective_position);
promise.set_value(out);
// we return a future from an already set promise...
return promise.get_future();
}
private:
AdjustOutput doAdjustInternal(const Position& position) {
// ... implementation
}
}
Method是一个入口,又调用了一个私有成员函数来执行操作(解耦?)。在调用doAdjustInternal得到返回值的时候,Method的输出就已经可以确定了。然后使用promise的set_value来获取out,再通过return来返回promise.get_future(),基于此让返回值构建函数返回的future。
调用这个Method的caller,可以马上通过Future::grt()获得结果。这个结果是直接通过调用方法函数得到的,不会存在阻塞。
下面是另外一种异步worker thread
的方式。
using namespace ara::com;
/**
* Our implementation of the RadarService
*/
class RadarServiceImpl : public RadarServiceSkeleton {
public:
Future<AdjustOutput> Adjust(const Position& position)
{
ara::core::Promise<AdjustOutput> promise;
auto future = promise.get_future();
// asynchronous call to internal adjust function in a new Thread
std::thread t(
[this] (const Position& pos, ara::core::Promise prom) {
prom.set_value(doAdjustInternal(pos));
},
std::cref(position), std::move(promise)).detach();
// we return a future, which might be set or not at this point...
return future;
}
private:
AdjustOutput doAdjustInternal(const Position& position) {
// ... implementation
}
}
上述代码中,实现了异步调用处理进程的方式。我们把实际上干活的函数封装在了一个lambda表达式中。
lambda表达式是在C++ 11 中引入的。可以理解为未命名的内联函数、闭包、函数内部无名的封装的代码块。
[]表示捕获,是说明表达式内可以使用的当前作用域内的信息,
[this]表示可以使用当前对象的成员。
Events
skeleton端的服务实现就是要通知别人事件的发生。
在skeleton端,每个事件都被封装在一个类里,在namespace skeleton下面。看起来和proxy那一端有很大的不同。(我觉得和Method比起来也很不同)。
class BrakeEvent {
public:
/**
* Shortcut for the events data type.
*/
using SampleType = RadarObjects;
void Send(const SampleType &data);
ara::com::SampleAllocateePtr<SampleType> Allocate();
/**
* After sending data you loose ownership and can’t access
* the data through the SampleAllocateePtr anymore.
* Implementation of SampleAllocateePtr will be with the
* semantics of std::unique_ptr (see types.h)
*/
void Send(ara::com::SampleAllocateePtr<SampleType> data);
};
值得注意的是有两种Send重载变体。Send是向外发送event数据的接口。
第一种就是直接把整个data引用发送出去(给binding,从当前进程的堆区?类似的,反正是存在本地的某个地方)。因为在调用send()后, data可能在服务端会变化或者被释放(重新赋值啊之类的)binding会在调用中对data进行复制(这样在客户端拿到数据的时候才不会乱掉)。
在第二种Send()中。data 变成了ara::com::SampleAllocateePtr。按照AP常见的作法,他会在代码中提供抽象的接口,然后在抽象接口背后提供明确的到现有的C++类型的映射。这里这个指针的背后是std::unique_ptr。
这里的意思是只有某一方可以持有这个指针。如果所有者想要把这个指针交出去选哟显示地通过std::move。
来看内存分配成员函数:
ara::com::SampleAllocateePtr<SampleType> Allocate();
这个方法, 返回一个指针,分配了用于存放一个样本单位event数据的内存。
这个指针可以用于存放数据,也可以放在第二个Send()里发出去。
这么设计的原因是:为了优化数据复制!!!
如果data特别大,比如是个vector队列(我能传图像吗 hh)。用第一种Send()。会在当前进程的堆区开一块内存来存放数据。
然后在Send()的时候,binding需要复制一份数据到用户可以访问的内存空间。
如果数据又大,频率又高,那就崩了。
使用Allocate()和第二种Send()就是为了避免复制。
Allocate()应该把数据存在能让收发两边都能访问的内存区域。比如共享内存区域。这块区域需要做读写端同步机制,保证数据一致性。
event提供方,调用Allocate(),然后向分配的内存区域写数据。然后在Send()里提交所有权。
下面是使用示例:
using namespace ara::com;
// our implementation of RadarService - subclass of RadarServiceSkeleton
RadarServiceImpl myRadarService;
/**
* Handler called at occurrence of a BrakeEvent
*/
void BrakeEventHandler() {
// let the binding allocate memory for event data...
SampleAllocateePtr<BrakeEvent::SampleType> curSamplePtr =
myRadarService.BrakeEvent.Allocate();
// fill the event data ...
curSamplePtr->active = true;
fillVector(curSamplePtr->objects);
// Now notify event to consumers ...
myRadarService.BrakeEvent.Send(std::move(curSamplePtr));
// Now any access to data via curSamplePtr would fail -
// we’ve given up ownership!
}
Fields
- 更新和通知 field 的变更
- 响应 Get()和 Set()的调用。
skeleton::fields::下的类
class UpdateRate {
public:
using FieldType = uint32_t;
/**
* Update equals the send method of the event. This triggers the
* transmission of the notify (if configured) to
* the subscribed clients.
*
* In case of a configured Getter, this has to be called at least
* once to set the initial value.
*/
void Update(const FieldType& data);
/**
* Registering a GetHandler is optional. If registered the function
* is called whenever a get request is received.
*
* If no Getter is registered ara::com is responsible for responding
* to the request using the last value set by update.
*
* This implicitly requires at least one call to update after
* initialization of the Service, before the service
* is offered. This is up to the implementer of the service.
*
* The get handler shall return a future.
*/
void RegisterGetHandler(std::function<ara::core::Future<FieldType>()>
getHandler);
/**
* Registering a SetHandler is mandatory, if the field supports it.
* The handler gets the data the sender requested to be set.
* It has to validate the settings and perform an update of its
* internal data. The new value of the field should than be set
* in the future.
*
* The returned value is sent to the requester and is sent via
notification to all subscribed entities.
*/
void RegisterSetHandler(std::function<ara::core::Future<FieldType>(
const FieldType& data)> setHandler);
};
Update方法来实现更改field的值。值更改后。binding会通知订阅者。binding会把传给他的值复制保存一份。
注册Setter 和 Getter
RegisterGetHandler
注册Get()的处理方法。这个处理方法是服务开发人员实现的。
客户端调用Get()的时候,binding就会来调用这里注册的方法。只有在接口描述里面定义了存在field getter
才会生成这种注册方法。
实际上,是否要实现个性化的Get()是可选的。一般来讲都不需要。binding会默认去取update()更新的最新值。
理论上使用这种GetHandler的理由是:如果计算一次field的结果很耗成本和时间,使用Gethandler可以在只有需要的时候把计算过程放在Gethandelr里来计算field。如果设置了on -change-notification
,注册Get handler也没有什么意义。
总结:我虽然提供了这个API,但是最好别用。用了后果自负。
RegisterSetHandler
与上面的Gethanler不同,希望开发人员都注册自己的SetHandler。这样可以在更新Field的值之前检测值的有效性。输入参数是clent希望设置的,返回值是经过Sethandler验证或者矫正的有效值。CM会根据SetHandler的返回值自动调用更新Fields值的程序,开发人员无需再在SetHandler里面显示调用Update()。
确保SetHandler存在
ara::com会确保SetHandler
存在。如果不存在会报unchecked error
.在OfferService()
之前,开发人员需要为每个fields注册“SetHandler”,如果setter在配置中开了的话,不然的话 CM会报“未检查set值”的错误 。
保证任何时候Field的值都有效
服务实现者需要在服务对客户端可见前就提供一个Field值。这样在客户注册订阅后,通过初始值通知(如果有的话)或者Get,可以先拿到一个有效值。
CM需要报一个“unchecked error”当发生以下情况:
服务端调用OfferService()
让服务可见,但是有的field没有被Update()。
这些field开启了“通知”(直接就发给订阅者了),或者使能了getter但是没有注册"GetHandler"(难道在gethandler 里面处理没初始值的情况也行?? hh).
注意:AP 模型支持定义field 初始值,存在一些生成变量之类的东西里。这样可以在Application代码中调用Update()把这些初始值传入。