7.0 KiB
7.0 KiB
tags, date created, date modified
| tags | date created | date modified |
|---|---|---|
| 星期四, 十一月 20日 2025, 11:58:41 晚上 | 星期四, 十一月 20日 2025, 11:59:25 晚上 |
2.3.1 事件总线架构与路由机制 (Event Bus Architecture & Routing Mechanism)
一、 约束输入与对齐 (Constraints & Alignment)
基于您提供的设计文档(特别是 05_任务调度器设计.md)和系统环境,我们面临以下硬性约束:
- 进程内通信 (In-Process):本节讨论的是同一个进程(
main_app)内部,不同 C++ 对象(模块)之间的交互。严禁引入 Socket、Pipe 或由 OS 调度的 IPC 机制(如 DBus/ZMQ),以避免微秒级的系统调用开销。 - 语言标准:必须兼容 C++14 (GCC 7.3)。
- 实时性要求:控制指令(如
StopModule)必须在 < 1ms 内到达目标模块。 - 全链路追踪:事件总线是
TraceID传递的关键载体,必须支持上下文的自动传播。
二、 权衡分析与选项呈现 (Trade-off Matrix)
议题 1:路由分发策略 (Dispatch Strategy)
| 选项 | A. 纯同步直接调用 (Synchronous Direct) | B. 纯异步队列 (Asynchronous Queued) | C. 混合双通道 (Hybrid Dual-Channel) (推荐) |
|---|---|---|---|
| 机制 | publish() 时直接在调用者线程遍历并执行所有回调函数。 |
publish() 将事件推入队列。后台线程池异步取出并执行回调。 |
提供 publishSync(高优指令)和 publishAsync(状态上报)两个接口。 |
| 延迟 | 最低 (微秒级)。无上下文切换,无排队。 | 较高。受队列深度和调度器负载影响。 | 灵活。关键指令零延迟,非关键消息不阻塞主业务。 |
| 死锁风险 | 高。如果回调函数里又发了新事件,容易导致递归死锁。 | 低。解耦了生产者和消费者。 | 中。需规范同步通道的使用场景。 |
| 适用场景 | 紧急停止、资源抢占。 | 日志上报、非关键状态更新。 | 生产环境标准解。 |
议题 2:订阅者模型 (Subscriber Model)
| 选项 | A. 泛型/模板回调 (Type-Erasure) (推荐) | B. 继承接口 (Inheritance) |
|---|---|---|
| 机制 | bus->subscribe<MyEvent>(lambda)。利用 std::function 和 std::type_index。 |
订阅者必须实现 IEventHandler<MyEvent> 接口。 |
| 耦合度 | 极低。模块不需要继承特定基类,只要函数签名对就行。 | 高。侵入性强,增加类层级复杂度。 |
| 灵活性 | 高。支持 Lambda,便于捕获 this 指针或上下文。 |
低。 |
| 性能 | 极高(现代编译器优化 std::function 很好)。 |
虚函数调用开销(微小)。 |
三、 基线确立与实施规范
为了兼顾雷达系统对指令的即时响应(如资源抢占)和状态处理的高吞吐(如海量模块状态变更),我们确立 C. 混合双通道 + 泛型回调 为设计基线。
1. 接口定义基线 (C++14)
我们定义一个强类型的、支持 TraceID 注入的接口。
class IEventBus {
public:
virtual ~IEventBus() = default;
/**
* @brief 订阅特定类型的事件
* @tparam EventType 事件结构体类型
* @param handler 回调函数,接收 const EventType&
*/
template <typename EventType>
void subscribe(std::function<void(const EventType&)> handler);
/**
* @brief 同步发布 (高优先级指令)
* @details 在当前线程立即执行所有订阅者。调用者会被阻塞直到所有处理完成。
* @param event 事件对象 (需继承自 BaseEvent 以携带 TraceID)
*/
template <typename EventType>
void publishSync(const EventType& event);
/**
* @brief 异步发布 (状态上报/非关键消息)
* @details 将事件放入无锁队列,由 EventBus 内部的 Worker 线程稍后处理。立即返回。
* @param event 事件对象
*/
template <typename EventType>
void publishAsync(const EventType& event);
};
2. 核心实现机制
- 同步通道 (
publishSync):- 实现:直接查找
std::unordered_map<std::type_index, std::vector<Handler>>。 - 锁策略:使用
std::shared_timed_mutex(读写锁)。发布时加读锁(允许多个事件同时发布,只要不修改订阅关系),订阅时加写锁。 - 死锁规避:严禁在
publishSync的回调中再次调用subscribe(修改订阅表)。允许递归调用publish,但需注意栈溢出风险。
- 实现:直接查找
- 异步通道 (
publishAsync):- 实现:维护一个
WorkQueue。由于事件类型各异,队列元素需使用std::function<void()>包装器(Type Erasure)来存储“执行动作”,而不是存储原始事件数据。 - 并发模型:
- 单分发线程 (默认):一个后台线程专门负责从队列取任务并执行。保证了同一事件的消费顺序。
- 队列选型:MoodyCamel
ConcurrentQueue(MPMC 无锁队列) 或boost::lockfree::queue。鉴于麒麟系统库现状,若无第三方库,使用std::deque+std::mutex+std::condition_variable也是可接受的(吞吐量在控制面不是瓶颈)。
- 实现:维护一个
3. TraceID 的隐式传递 (2.3.2 预埋)
- 基线要求:
IEventBus不仅仅是搬运工,它还是上下文的管理者。 - 同步发布时:
TraceID自然随着线程栈传递。 - 异步发布时:
publishAsync必须在入队时捕获当前线程的TraceID,并将其打包到 Lambda 中。在出队执行时,先恢复该TraceID到线程本地存储 (TLS),再执行回调。- 这解决了异步调用导致追踪链断裂的经典难题。
4. 异常安全边界
- 基线决策:
EventBus必须捕获订阅者抛出的所有异常。 - 论证:一个模块的回调函数崩溃(throw exception)绝不能导致发消息的模块(如调度器)崩溃,也不能中断后续其他模块接收该事件。
- 行为:
try-catch包裹每个 handler 的调用。捕获异常后,记录ERROR日志(包含 TraceID),然后继续执行下一个 handler。
总结与下一步行动
我们确立了 2.3.1 事件总线 的基线:
- 架构:混合双通道 (Sync/Async)。
- 实现:泛型 Pub/Sub,基于 C++14。
- 安全:异常隔离 + 读写锁保护。
有了这个“神经系统”,我们可以开始讨论在这上面跑什么“数据包”了。为了让全链路追踪真正生效,我们需要定义事件的“信封”格式。
下一步建议:进入 2.3.2 全链路追踪上下文传递 (Trace Context Propagation),详细定义 BaseEvent 结构和 TraceContextGuard 的实现机制。这是之前多次提到的 TraceID 落地的具体技术点。
提问:您是否确认 “混合双通道 + 泛型回调” 的事件总线基线?确认后我们将深入 2.3.2。