☞返回总目录
5.4.7 事件(Events)
在骨架侧,服务实现负责通知事件的发生。如 5.4.2 RadarService Skeleton Class 所示,骨架为每个事件提供一个事件包装类的成员。骨架的事件包装类与代理的事件包装类看起来明显不同。
在骨架端,服务特定的事件包装类在骨架命名空间正下方的事件命名空间内定义。以示例事件BrakeEvent
的事件包装类为例:
class BrakeEvent
{
public:
/**
* 事件数据类型的快捷方式。
*/
using SampleType = RadarObjects;
ara::core::Result<void> Send(const SampleType &data);
ara::core::Result<ara::com::SampleAllocateePtr<SampleType>> Allocate();
/**
* 发送数据后,您将失去所有权,并且无法再通过 SampleAllocateePtr 访问数据。
* SampleAllocateePtr 的实现将具有 std::unique_ptr 的语义(请参阅 types.h)
*/
ara::core::Result<void> Send(ara::com::SampleAllocateePtr<SampleType> data);
};
与代理侧类似,using
指令为事件的具体数据类型引入了通用名称SampleType
。我们提供了Send()
方法,有两个不同变体,用于发送新的事件数据。
- 第一个变体
第一个变体接受一个SampleType
的引用。这种变体很直接:事件数据已由服务应用程序开发人员在某处分配,并通过引用传递给Send()
的绑定实现。在调用Send()
返回后,调用者的数据可能会被删除或更改。绑定实现将在调用中进行复制。
疑问:Send()
不是服务提供者用来在事件发送更新时,把事件数据发送给其订阅者的吗?怎么调用者的数据可能会被删除或更改呢?
- 第二个变体及相关概念
Send()
的第二个变体也有一个名为data
的参数,但它是类型为ara::com::SampleAllocateePtr<SampleType>
。根据一般方法,只提供抽象接口,并最终建议映射提供到现有 C++ 类型(请参阅第 4.6 节),在这里引入的这个指针类型应表现得像std::unique_ptr<T>
。这意味着:只有一方可以持有指针 —— 如果所有者想要放弃它,他必须通过std::move()
显式地进行。
为了理解这个概念,我们必须首先查看事件包装类中的第三个方法:
ara::com::SampleAllocateePtr<SampleType> Allocate();
Allocate()
提供了一种为事件数据样本分配内存的方法,它返回一个ara::com::SampleAllocateePtr<SampleType>
的智能指针,该指针指向了分配的内存,可以在分配的内存写入一个事件数据样本。然后,我们可以将这个返回的智能指针传递给即将调用的Send()
的第二个变体。
问题:为什么要让绑定实现Allocate()
为即将给发送给潜在消费者的事件数据进行内存分配呢?
答案很简单:有优化数据复制的可能性。
下面这个夸张的例子可以让事情更清楚:假设我们这里讨论的事件(类型为RadarObjects
)可能非常大,即它包含一个向量,这个向量可能会变得非常大(比如说几百千字节)。在Send()
的第一个变体中,你将在应用程序进程的堆上自行分配这个事件的内存。然后 —— 在调用Send()
的第一个变体期间 —— 绑定实现Send()
必须将这个事件数据从(私有的)进程堆复制到一个消费者可以访问的内存位置。如果要复制的事件数据非常大,并且这种事件发生的频率很高,那么仅仅是数据复制的运行时间就可能会造成影响。
Allocate()
和发送事件数据的第二个变体(Send(SampleAllocateePtr<SampleType>)
)相结合的想法是最终避免这种复制!一个智能的绑定实现可能会以某种方式实现Allocate()
方法,使得它在一个位置分配内存,在这个位置上写入者(服务 / 事件提供者)和读取者(服务 / 事件消费者)都可以直接访问它!所以设计了一个ara::com::SampleAllocateePtr<SampleType>
的指针,它指向接收者附近的内存。这样的双方都可以直接访问的位置通常被称为 “共享内存”。为了数据一致性,对这样的区域的访问应该在读取者和写入者之间进行同步。
这就是为什么Allocate()
方法返回这样一个带有它所指向的数据的单一 / 唯一用户方面的智能指针:在潜在的写入者(服务 / 事件提供者端)调用了Allocate()
之后,只要他将其交给Send()
的第二个变体,在那里他明确地放弃所有权,他就可以访问 / 写入所指向的数据!这是必要的,因为在调用之后,读取者将访问数据并且需要一个一致的数据视图。
以下是示例代码:
using namespace ara::com;
// 我们对 RadarService 的实现 - RadarServiceSkeleton 的子类
RadarServiceImpl myRadarService;
/**
* 在发生 BrakeEvent 时调用的处理程序
*/
void BrakeEventHandler() {
// 让绑定为事件数据分配内存...
SampleAllocateePtr<BrakeEvent::SampleType> curSamplePtr =
myRadarService.BrakeEvent.Allocate();
// 填充事件数据...
curSamplePtr->active = true;
fillVector(curSamplePtr->objects);
// 现在通知消费者事件...
myRadarService.BrakeEvent.Send(std::move(curSamplePtr));
// 现在通过 curSamplePtr 访问数据将失败 -
// 我们已经放弃了所有权!
}