Skip to content
dgliu edited this page Feb 14, 2026 · 1 revision

常见问题 (FAQ)


Q: MCCC 与 eventpp / EnTT / sigslot 等事件库有什么区别?

A: MCCC 与通用事件库的定位不同,核心差异如下:

特性 MCCC eventpp / EnTT / sigslot
并发模型 无锁 MPSC/SPSC 通常需要外部加锁或仅支持单线程
优先级准入 三级背压 (HIGH/MEDIUM/LOW)
堆分配 零堆分配 (FixedVector/FixedString/FixedFunction) 依赖 std::function / std::vector
规范合规 MISRA C++ 导向 通用 C++ 风格
目标场景 安全关键嵌入式系统 (工控/机器人/激光雷达) 通用桌面/游戏/服务端应用
消息传输 值语义环形缓冲 (trivially_copyable) 指针/引用传递
编译期配置 队列深度/缓存行/单核模式等宏控制 运行时配置为主

如果项目无实时性要求、无内存约束、无安全合规需求,通用事件库更简单易用。MCCC 专为需要确定性延迟和资源可控的嵌入式场景设计。


Q: 可以使用多个消费者吗?

A: MCCC 的核心架构是 MPSC (Multi-Producer, Single-Consumer)。单条总线仅允许一个消费者线程调用 ProcessBatch() / ProcessBatchWith()

如果需要多个消费者,有以下方案:

  1. 多总线实例: 为每个消费者创建独立的 AsyncBus 实例,生产者向多条总线分别发布。
// 消费者 A 的总线
auto& busA = AsyncBus<PayloadA>::Instance();
// 消费者 B 的总线
auto& busB = AsyncBus<PayloadB>::Instance();
  1. 扇出模式: 单一消费者从总线取出消息后,根据类型转发到不同的处理队列。

  2. Component 多回调: 注册多个回调 (通过 Subscribe<T>()),在同一消费者线程内分发给不同的处理逻辑。虽然是单线程处理,但逻辑上实现了多消费者的效果。

不建议多线程并发调用 ProcessBatch(),这会导致未定义行为。


Q: Component 和 StaticComponent 有什么区别? 应该选哪个?

A:

特性 Component StaticComponent
订阅方式 运行时动态 (SubscribeSafe()) 编译期静态 (MakeVisitor())
取消订阅 支持 (Unsubscribe()) 不支持 (编译期绑定)
生命周期安全 weak_ptr 自动检测 调用者保证
运行时开销 shared_mutex 读锁 + 回调查找 零开销 (std::visit)
适用场景 需要动态增减订阅的组件 消息类型在编译期已确定的组件

选择指南:

  • 如果组件在运行期间需要动态订阅/取消订阅不同类型的消息,或者组件的生命周期不确定 (可能在回调触发前析构),使用 Component + SubscribeSafe()

  • 如果组件处理的消息类型在编译期已完全确定,且组件生命周期可控,使用 StaticComponent + MakeVisitor()。性能优势: 19%~42%。


Q: ProcessBatch 和 ProcessBatchWith 有什么区别?

A:

ProcessBatch

  • 通过回调表 (DispatchTable) 分发消息
  • 运行时查找: 根据 variantindex() 定位回调列表
  • 需要 shared_mutex 读锁保护回调表 (因为 Subscribe/Unsubscribe 可能并发修改)
  • 支持运行时动态增减回调

ProcessBatchWith

  • 通过 std::visit + Visitor 模式分发消息
  • 编译期分发: 编译器根据 variant 类型生成跳转表
  • 无锁: 不依赖回调表,不需要 shared_mutex
  • 消息处理逻辑必须在编译期确定

性能对比

场景 ProcessBatch ProcessBatchWith 加速
MPSC 160 ns/msg 129 ns/msg 19%
SPSC 161 ns/msg 93 ns/msg 42%

建议: 如果不需要运行时动态订阅,优先使用 ProcessBatchWith


Q: 如何在 MCU 上使用 MCCC?

A: MCCC 通过编译期宏适配 MCU 环境。推荐配置:

// CMakeLists.txt 或编译选项
-DMCCC_QUEUE_DEPTH=1024        // 或 4096,根据 SRAM 大小调整
-DMCCC_SINGLE_PRODUCER=1       // 大多数 MCU 场景为 SPSC
-DMCCC_SINGLE_CORE=1           // 单核 MCU
-DMCCC_I_KNOW_SINGLE_CORE_IS_UNSAFE=1  // 必须显式确认
-DMCCC_CACHELINE_SIZE=0        // 无 D-Cache 的 MCU 不需要缓存行对齐
-DSTREAMING_DMA_ALIGNMENT=4    // 自然对齐即可

关键注意事项:

  1. SRAM 预算: MCCC_QUEUE_DEPTH * sizeof(Envelope) 是主要内存消耗。Envelope 大小取决于 PayloadVariant 中最大类型的大小。先确认 SRAM 容量再确定队列深度。

  2. 单核模式: MCCC_SINGLE_CORE=1 使用 relaxed 内存序 + atomic_signal_fence,消除硬件内存屏障。在单核 MCU 上完全安全 (ISR 与主循环之间的可见性由编译器屏障保证)。

  3. 编译器要求: 需要 C++17 支持。GCC 7+ / Clang 5+ / ARM Compiler 6+ 均支持。

  4. -fno-exceptions -fno-rtti: MCCC 完全支持无异常和无 RTTI 编译,不依赖任何异常或动态类型信息。

  5. ISR 中使用: ISR 中可安全调用 PublishFast() (在 SPSC 单核模式下为 wait-free,无 CAS,无系统调用)。不要在 ISR 中调用 Publish() (在 FULL_FEATURED 模式下包含时间戳采集和统计更新)。


Q: MCCC 是线程安全的吗?

A: 分操作讨论:

操作 线程安全性 说明
Publish() / PublishWithPriority() / PublishFast() 多线程安全 (lock-free) 多个线程可并发调用 (MPSC 模式)
ProcessBatch() / ProcessBatchWith() 单线程调用 仅允许一个消费者线程调用
Subscribe() / Unsubscribe() 需要写锁 内部使用 shared_mutex,与 ProcessBatch 的读锁互斥
GetStatistics() 多线程安全 原子加载,返回快照
SetPerformanceMode() 多线程安全 原子 store
SetErrorCallback() 非线程安全 应在启动阶段设置,不应在运行时修改

关键约束:

  • ProcessBatch() / ProcessBatchWith() 严禁多线程并发调用。MCCC 是 MPSC 架构,消费端为单线程设计。
  • Subscribe() / Unsubscribe() 会获取写锁,会阻塞 ProcessBatch() 的读锁。频繁调用会影响消费者吞吐量。建议在初始化阶段完成所有订阅。
  • 在 SPSC 模式 (MCCC_SINGLE_PRODUCER=1) 下,Publish() 也仅允许单线程调用。

Q: 队列满了会怎样?

A: 行为取决于性能模式和消息优先级:

FULL_FEATURED / NO_STATS 模式 (背压启用)

队列未物理满之前,背压机制根据水位和优先级决定是否准入:

队列水位 LOW MEDIUM HIGH
< 60% 入队 入队 入队
60%~80% 丢弃 入队 入队
80%~99% 丢弃 丢弃 入队
>= 99% 丢弃 丢弃 大概率丢弃

消息被背压丢弃时:

  1. Publish() / PublishWithPriority() 返回 false
  2. 统计计数器 total_backpressure_drops 递增 (仅 FULL_FEATURED)
  3. 如果设置了 SetErrorCallback(),错误回调被触发

BARE_METAL 模式 (背压关闭)

所有消息直接尝试入队。队列物理满时:

  1. Publish() 返回 false
  2. 消息静默丢弃,无回调通知

处理建议

auto& bus = AsyncBus<MyPayload>::Instance();

// 设置错误回调,监控丢弃情况
bus.SetErrorCallback([](const char* msg) {
    // 记录日志或触发告警
    LOG_WARN("Bus: %s", msg);
});

// 定期检查统计
auto stats = bus.GetStatistics();
if (stats.queue_utilization > 0.8) {
    // 队列水位过高,考虑:
    // 1. 增大 MCCC_QUEUE_DEPTH
    // 2. 提高消费者处理速度
    // 3. 降低生产者发送频率
}

关键消息应使用 MessagePriority::HIGH,确保在背压场景下优先入队。