MergeCell 合并机制:双平台事件流去重
项目链接:github.com/DeguiLiu/co...
同一目标、同一信号的事件可能以极高频率到达(传感器 1 kHz 上报,消费方只要 100 Hz 的最新值)。逐个传递浪费------消费方只关心最新值。需要一个合并单元:把同一 (target, signal) 的后续事件合并进单个槽位,只保留最新状态。
naive 的方案是锁保护一个槽位,但 ISR 上下文不能持有锁。MergeCell 用单个 std::atomic 状态 + CAS 状态机实现无锁合并。
四态状态机
四个状态对应四种并发角色,全部通过 compare_exchange_strong 转移:
| 状态 | 持有者 | 允许的下一个动作 |
|---|---|---|
Empty |
无 | try_publish 置入新事件 |
Published |
已 post 的 Event* | try_acquire_merge(覆写)/ take_owning(取走) |
Merging |
认领 merge 的 producer | release_merge 回到 Published |
Consuming |
取走的 dispatcher | release_empty 回到 Empty |
CAS 失败永不阻塞
与 Treiber 栈不同,MergeCell 的 CAS 失败不重试、不旋转。每个转移方法失败就返回 false,调用方走降级路径:
cpp
bool try_acquire_merge(Event*& queued) noexcept
{
MergeCellState expected = MergeCellState::Published;
if (!state_.compare_exchange_strong(expected, MergeCellState::Merging,
std::memory_order_acq_rel)) {
return false; // 不旋转,直接失败
}
queued = event_;
return true;
}
合并是 best-effort 的,错过一次合并只是多入队一个事件。这解释了为什么 merge_wins > 0 的断言是错的------两个 producer 可以都 publish 成功、都来不及 merge,merge_wins == 0 完全合法。
内存序:acq_rel 边界 + relaxed 数据
MergeCell 的经典陷阱是状态原子与数据指针分离 。状态在 state_ 原子上,payload 在普通 Event* 上------必须用内存屏障把"状态转移"和"数据可见性"绑定:
cpp
// producer 置入:先 CAS 拿到 Published,再写数据
MergeCellState expected = MergeCellState::Empty;
if (!state_.compare_exchange_strong(expected, MergeCellState::Published,
std::memory_order_acq_rel)) {
return false;
}
event_ = e; // relaxed:acq_rel 边界已排序这次发布
try_publish 的 CAS 是 acq_rel------release 侧保证 event_ 写在状态翻转之前可见,后续任何通过 Published 状态的 acquire(如 try_acquire_merge 的 CAS)都能正确读到 event_。MergeCell::state() 用 memory_order_acquire 读,保证读状态时能看到对应 epoch 的数据。
引用计数配合
合并槽位持有的是已 post 的引用计数事件 (event_ref_inc 过)。dispatcher 在 Consuming 状态下 take_owning 拿到句柄,消费完必须 event_gc() 恰好一次,再 release_empty 归位。合并覆写时旧事件被新事件替代、旧事件回池;取走消费时槽位归空、事件最终回池。无泄漏、无双重释放。
提交记录:d6cc476(MergeCell 成员初始化 + acq_rel 定型)