0. 引言
本文展示一个实践路径:以轻量级 C++ 事件库 eventpp 为核心,设计并实现一个面向嵌入式的、可移植的 Active Object(AO)事件驱动架构。该架构满足以下目标:
- 跨平台兼容:单套代码在 RT-Thread(或裸机)与 ARM-Linux 下均可编译与运行
- 开源免费:使用 eventpp 与自研轻量库,避免商业授权成本
- 可定制:通过策略(Policy)层注入不同的锁、容器、优先级实现适配不同平台
1. 为什么选择 eventpp
eventpp 是一个纯头文件的现代 C++ 事件库,特点非常契合嵌入式与资源受限环境的需求:
- 纯头文件、无运行时依赖,易于移植到 RT-Thread、裸机或交叉编译的 ARM-Linux
- 提供 CallbackList、EventDispatcher、EventQueue 三大功能模块,能满足同步与异步事件场景
- 支持 Policy 注入,可替换底层容器、锁、优先级策略 --- 便于将内存/并发策略绑定到平台要求
- 小巧、可读、易于裁剪:便于做静态分配或替换动态容器
总结:eventpp 不是一个完整的 RTOS 或高级状态机框架,但作为"事件发布/订阅 + 异步队列"的基石非常合适。
2. eventpp 三大核心组件速览
-
CallbackList
- 基础的回调列表,可注册任意可调用对象(函数、Lambda、成员函数),在回调执行过程中可安全添加/删除。适合"固定事件、原型各异"的简单场景。
-
EventDispatcher
- 类型到回调列表的映射:同步按事件类型分发,所有监听器立刻执行。支持自定义 Policy,从复杂事件对象中抽取事件类型。
-
EventQueue
- 异步版 EventDispatcher:先
enqueue
入队,再process
批量分发。支持跨线程wait()
/process()
,也可基于自定义 Policy 实现优先级调度。
- 异步版 EventDispatcher:先
这些组件可以组合成 AO(Active Object)模型:每个 AO 维护自己的 EventQueue(或多个队列),独立线程消费事件并驱动状态机或回调。
3. 使用速览(示例)
3.1 CallbackList 简单示例
cpp
#include <eventpp/callbacklist.h>
eventpp::CallbackList<void(int)> cbList;
auto h1 = cbList.append([](int x){ printf("A: %d\n", x); });
auto h2 = cbList.append([](int x){ printf("B: %d\n", x); });
cbList(42); // 输出 A: 42 B: 42
cbList.remove(h2);
cbList(7); // 只输出 A: 7
3.2 EventDispatcher 基础用法
cpp
#include <eventpp/eventdispatcher.h>
enum class Sig { Start, Stop };
eventpp::EventDispatcher<Sig, void(int), std::map> dispatcher;
dispatcher.appendListener(Sig::Start, [](int v){ printf("Start %d\n", v); });
dispatcher.dispatch(Sig::Start, 123); // 输出 Start 123
3.3 EventQueue 异步队列
cpp
#include <eventpp/eventqueue.h>
eventpp::EventQueue<std::string> queue;
queue.appendListener([](const std::string &s){
printf("Got: %s\n", s.c_str());
});
queue.enqueue("Hello");
queue.enqueue("World");
queue.process(); // 输出 Got: Hello Got: World
4. 内存策略:静态分配与零动态分配
在 RT-Thread 或硬实时路径中要保证可测的 WCET 与避免内存抖动,应优先使用静态或预分配结构存放事件。以下给出两种常见策略与实现样例。
- 静态环形缓冲(单生产者单消费者 / 多生产者场景需额外锁或 lock-free 结构)
- 对象池(预分配固定数量事件对象,支持复用)
- 可控的少量动态内存(仅在初始化阶段分配)在资源非常受限时也可接受
静态环形缓冲示例(已经在问题中给出,这里补充线程安全注意):
cpp
// StaticRing.h
#include <atomic>
#include <cstddef>
template<typename T, size_t N>
class StaticRing {
static_assert(N >= 2, "N must be >= 2");
T buffer[N];
std::atomic<size_t> head{0}, tail{0};
public:
bool enqueue(const T &v) {
size_t t = tail.load(std::memory_order_relaxed);
size_t next = (t + 1) % N;
if(next == head.load(std::memory_order_acquire)) {
return false; // 队列已满
}
buffer[t] = v;
tail.store(next, std::memory_order_release);
return true;
}
bool dequeue(T &out) {
size_t h = head.load(std::memory_order_relaxed);
if(h == tail.load(std::memory_order_acquire)) {
return false; // 队列为空
}
out = buffer[h];
head.store((h + 1) % N, std::memory_order_release);
return true;
}
};
注意:
- 对于多生产者/多消费者,需要额外的原子或锁保护(或使用专门的 lock-free 队列实现)
- 内存拷贝成本:如果事件对象较大,建议使用小事件句柄(ID + 指针到对象池)或移动语义
- 避免在 ISR 中进行占用长时间的操作:在 ISR 中只做入队与唤醒,处理留给 AO 线程
5. 平台抽象层(PAL):解耦 RTOS / Linux 实现
为了实现同一套 AO 代码在 RT-Thread 和 ARM-Linux 下工作,推荐引入一个 PAL(Platform Abstraction Layer)最小 API:
- 线程 / 任务创建:PalThread::create(...)
- 互斥锁 / 递归锁:PalMutex / PalRecursiveMutex
- 信号量 / 事件:PalSemaphore
- 中断安全入队的 primitive(如果 RTOS 提供 ISR-safe API,可包装)
- 时间与延时:PalTime::sleepMs, now
示例接口(伪头文件):
cpp
// pal.h (伪接口)
#pragma once
#include <functional>
#include <cstdint>
namespace pal {
using ThreadFunc = std::function<void()>;
struct ThreadHandle { /* opaque */ };
class Thread {
public:
static ThreadHandle create(const char* name, ThreadFunc func, int priority, size_t stackSize = 4096);
static void join(ThreadHandle);
// ...
};
class Mutex {
public:
Mutex();
void lock();
bool try_lock();
void unlock();
};
class Semaphore {
public:
Semaphore(unsigned initial = 0);
void acquire();
bool try_acquire();
void release();
};
} // namespace pal
在 RT-Thread 下实现这些接口时要注意 ISR-safe API(例如 rt_sem_release_from_isr);在 Linux 下用 pthreads 或 std::thread/std::mutex 实现。
6. EventQueue 的策略注入(Policy)与 AO 模型实现
利用 eventpp 的 Policy 注入机制,我们可以为不同平台定制底层锁、容器和优先级策略,例如把静态环形缓冲注入到 EventQueue。
示例 Policy 定义:
cpp
// MyPolicies.h
#include <eventpp/eventqueue.h>
// 假设已包含 StaticRing<Event> 和平台 PalMutex
using RtStaticPolicy = eventpp::EventQueuePolicy<
/*ContainerBuilder*/ eventpp::policy::VectorLikeBasedContainer<StaticRingWrapper>,
/*Lock*/ PalMutex,
/*PriorityPolicy*/ eventpp::DefaultPriorityPolicy
>;
// 使用示例(伪代码,视具体 eventpp 版本接口而定)
using AoEventQueueRt = eventpp::EventQueue<Event, void(const Event&), RtStaticPolicy>;
Active Object 的实现伪代码如下(补充完整细节):
cpp
class ActiveObject {
public:
ActiveObject(const char* name)
: running(true)
{
threadHandle = pal::Thread::create(name, [this](){ run(); }, /*priority=*/10, /*stack=*/4096);
}
~ActiveObject() {
running = false;
eventSem.release(); // wake up to exit
pal::Thread::join(threadHandle);
}
// 普通上下文发事件
bool post(const Event &e) {
if(eventQueue.enqueue(e)) {
eventSem.release();
return true;
} else {
++stats.dropped;
return false;
}
}
// ISR 中调用(必须使用 ISR-safe enqueue 与 notify)
bool postFromIsr(const Event &e) {
if(eventQueue.enqueueFromIsr(e)) { // 需要容器/策略支持
isrFlag.store(true, std::memory_order_release);
// 使用 ISR-safe 唤醒
eventSem.releaseFromIsr();
return true;
} else {
++stats.dropped;
return false;
}
}
private:
void run() {
while(running) {
eventSem.acquire();
// 处理直到队列为空或处理批量
eventQueue.process();
}
}
AoEventQueueRt eventQueue;
pal::Semaphore eventSem;
std::atomic<bool> isrFlag{false};
std::atomic<bool> running{true};
pal::ThreadHandle threadHandle;
// 统计、状态机、回调列表等
};
要点:
- eventQueue.enqueueFromIsr 与 Semaphore::releaseFromIsr 的可用性取决于具体 PAL 与容器实现
- 在 RTOS/裸机路径,确保 ISR 中的操作为最小耗时且可中断安全
- AO 的 run() 中应尽量避免长阻塞(除非这是设计意图),可在处理每个事件时记录处理时间用于 WCET 测量
7. ISR 与 AO 协作:从中断安全到唤醒机制
设计 AO+ISR 协作时的典型模式:
- 在 ISR 中构建或引用事件(尽量小),调用 ISR-safe enqueue(或写入环形缓冲直接内存写入)
- 在 ISR 中仅进行必要的唤醒(如给信号量/事件标志),避免调用复杂的调度逻辑
- AO 线程被唤醒后逐条或批量处理事件并执行较长/不安全的操作(比如动态内存、文件操作等)
注意事项:
- 事件对象的内存管理:ISR 中最好只写入小而固定大小的数据或索引到对象池,避免在 ISR 中 new/delete
- 优先级反转:如果 AO 与 ISR 之间有锁竞争,需防止优先级反转,使用 RTOS 提供的优先级继承或选择无锁方案
- 批处理以减少上下文切换:在 AO 中 process() 可以一次处理 N 条事件,或者处理直到队列为空,平衡延迟与吞吐
8. 优先级、调度与避免饥饿(Priority Policy)
如果系统包含高/中/低优先级事件,需要在 EventQueue 层支持优先级:
常见方案:
- 多队列(per-priority queue):高优先级队列先处理,低优先级队列后处理,可防止高频低优先任务饥饿低优先任务(通过令牌/轮询策略)
- 单队列带优先排序:插入时用比较器排列,缺点是插入复杂度高且插队可能破坏 WCET 可测性
- 混合:固定优先级数目的环形缓冲数组 + 限额处理策略(限制连续处理高优先事件的数量)
示例:多队列 + 轮询限额(伪代码)
cpp
void process() {
int highCount = 0;
while(true) {
if(dequeueFromHighQueue(event)) {
handle(event);
++highCount;
if(highCount >= HIGH_LIMIT) {
// 让出一次机会处理中/低优先
if(dequeueFromMidQueue(event)) { handle(event); }
if(dequeueFromLowQueue(event)) { handle(event); }
highCount = 0;
}
continue;
}
if(dequeueFromMidQueue(event)) { handle(event); continue; }
if(dequeueFromLowQueue(event)) { handle(event); continue; }
break;
}
}
要点:
- WCET:引入优先级后必须对最坏情况执行路径重新评估
- 可测性优先:在硬实时场景下选择更可控(固定时间限制/批量上限)的策略
9. 部署示例:RT-Thread 与 ARM-Linux 的实现要点
RT-Thread 实现注意点:
- 使用 rt_thread_create、rt_sem_take/release、rt_mutex_* 等替代 PAL 接口
- ISR 中使用 rt_sem_release_fromISR(或 rt_sem_release + rt_hw_interrupt_mask/unmask)
- 在 bsp 层做好堆栈、内存池的静态分配,避免动态分配(new/malloc)
ARM-Linux 实现注意点:
- 使用 std::thread / pthreads / std::mutex / std::condition_variable 或者基于 epoll 的事件循环
- 如果需要硬实时级别,可使用 PREEMPT_RT 或基于 rtprio 的实时进程来运行 AO 线程
- 内存策略:在进程初始化时使用 malloc 大对象池,运行时避免再分配
两端共享代码实践:
- 把核心 AO、eventpp 使用、状态机逻辑放入可编译在两端的库(仅依赖 STL 或做条件编译)
- PAL 在不同平台实现不同文件,通过 cmake 或 makefile 在交叉编译时选择
示例目录结构(建议):
- src/core/ (AO、事件、状态机、policy glue)
- src/pal/rtthread/ (RT-Thread 的 PAL 实现)
- src/pal/linux/ (Linux 的 PAL 实现)
- examples/ (运行示例)
- tests/ (单元与集成测试)
10. 示例架构图
时序图
ISR PAL EventQueue AO CB postFromIsr(e) enqueue ISR-safe signal / wakeup process() dispatch callbacks ISR PAL EventQueue AO CB
类关系
ActiveObject +post(Event) +postFromIsr(Event) -run() EventQueue PalThread PalMutex
优先级多队列示意
ISR Producers high mid low process QH ISR1 QM ISR2 QL ThreadProd Active Object Handler
11. 与其它框架的对比与权衡
-
QP/C++(付费)
- 优点:成熟的 AO 框架、事件池、状态机支持、面向嵌入式的设计、硬实时适配能力强
- 缺点:授权成本、学习曲线、集成与裁剪成本
-
eventpp + 自研 PAL + 对象池(本文方案)
- 优点:零成本、极简、可裁剪、跨平台、可控内存分配
- 缺点:需要自行完成对象池、状态机、严格的实时保障需要手工设计
选择要点:
- 如果团队需要商用支持、成熟工具链与硬实时保障,QP/C++ 更合适
- 如果希望快速上手、跨平台且避免授权成本,eventpp+自研方案更灵活
12. 总结与建议
本文给出一种基于 eventpp 的轻量级 AO 模式实践,适用于对可移植性与内存可控性有较高要求的嵌入式项目。关键建议:
- 核心事件分发逻辑使用 eventpp,其 policy 注入能力让跨平台实现更简单
- 在 RTOS/裸机路径优先使用静态/预分配结构,避免中断与任务中动态分配
- 设计 PAL,隔离平台差异,保持核心逻辑可复用
- 对优先级、饥饿与 WCET 做专门测试与测量,并在设计中加上容错策略(如丢弃策略、统计报警)
- 对性能要求极高或强实时约束的场景,慎重评估是否需要更底层(内核级)支持或采用成熟商业框架