15. 事件过滤与聚合:事件过滤器模式、事件聚合器、事件窗口计算
好,咱们今天聊点实战中特别常用的东西——事件过滤与聚合。
你想想看,一个嵌入式系统跑起来,事件那是满天飞。按键按下、传感器数据、定时器超时、网络包到达……如果每个事件都去处理,CPU 早就累趴下了。更关键的是,很多事件其实是「噪音」,或者需要攒一批再处理才有意义。
我个人习惯把这类问题拆成三个模式来解决:事件过滤器、事件聚合器、事件窗口计算。它们各司其职,组合起来能搞定 90% 的复杂场景。
15.1 事件过滤器模式:把噪音挡在门外
说白了,过滤器就是一道门。符合条件的事件放进来,不符合的直接丢掉。
我在项目中遇到过这样一个坑:一个温度传感器,每 100ms 上报一次数据。但传感器本身有噪声,偶尔会跳变到离谱的值,比如 25°C 突然跳到 85°C。如果不加过滤,系统就会误判为过热,触发报警甚至关机。
核心思路:过滤器不改变事件内容,只做「通过/丢弃」的决策。
常见的过滤策略有几种:
- 阈值过滤:超过某个范围直接丢弃。比如温度 > 100°C 或 < -40°C 视为无效。
- 变化率过滤:相邻两次事件差值过大,可能是噪声,先丢一次看看。
- 重复过滤:短时间内收到相同的事件,只保留第一个。
来看一个简单的过滤器实现:
// 事件过滤器:基于阈值 + 变化率
typedef struct {
int32_t last_valid_value;
uint32_t last_tick;
int32_t max_change_rate; // 每 tick 最大允许变化
int32_t min_threshold;
int32_t max_threshold;
} event_filter_t;
int event_filter_apply(event_filter_t *filter, int32_t value, uint32_t tick) {
// 阈值检查
if (value < filter->min_threshold || value > filter->max_threshold) {
return 0; // 丢弃
}
// 变化率检查
uint32_t dt = tick - filter->last_tick;
if (dt > 0) {
int32_t delta = value - filter->last_valid_value;
if (delta < 0) delta = -delta;
if (delta / dt > filter->max_change_rate) {
return 0; // 变化太快,可能是噪声
}
}
// 更新状态
filter->last_valid_value = value;
filter->last_tick = tick;
return 1; // 通过
}
我的经验:过滤器的参数别写死。我习惯把它们做成可配置的,放在结构体里,这样调试阶段可以动态调整阈值,不用重新编译。
15.2 事件聚合器:把零散事件攒成一批
有些场景下,单个事件没什么用,攒够一批才有意义。比如:
- 按键去抖:连续 5 次采样都是低电平,才认为按键真的按下了。
- 数据打包:收到 10 个传感器数据后,统一发一次网络包。
- 状态确认:连续 3 次报警事件,才触发真正的报警动作。
事件聚合器的核心就是「等」——等数量够了,或者等时间到了。
// 事件聚合器:按数量聚合
typedef struct {
uint32_t count_threshold; // 触发阈值
uint32_t current_count;
uint32_t timeout_ms; // 超时时间(防止永远等不到)
uint32_t start_tick;
void (*on_aggregated)(void *context);
void *context;
} event_aggregator_t;
int event_aggregator_push(event_aggregator_t *agg, uint32_t tick) {
if (agg->current_count == 0) {
agg->start_tick = tick;
}
agg->current_count++;
// 检查是否达到阈值
if (agg->current_count >= agg->count_threshold) {
agg->on_aggregated(agg->context);
agg->current_count = 0;
return 1;
}
// 检查是否超时
if (tick - agg->start_tick > agg->timeout_ms) {
// 超时了,把当前这批也处理掉(虽然没攒够)
agg->on_aggregated(agg->context);
agg->current_count = 0;
return 1;
}
return 0;
}
注意:聚合器一定要加超时机制。我曾经在一个项目中忘了加,结果某个事件源突然停了,聚合器永远等不到阈值,数据就卡死了。嗯,那次排查花了我大半天。
15.3 事件窗口计算:滑动窗口里的统计
窗口计算比聚合更高级一点。它不光攒事件,还要在窗口内做统计——比如最近 1 秒内的平均值、最大值、最小值。
我举个例子你就明白了:
- 你想知道过去 5 分钟内 CPU 的平均负载。
- 你想检测最近 10 次采样中是否有超过 3 次异常。
- 你想计算最近 100ms 内接收到的网络包数量。
这些都需要一个「滑动窗口」来维护历史数据。
// 滑动窗口计算器
#define WINDOW_SIZE 100
typedef struct {
int32_t buffer[WINDOW_SIZE];
uint32_t head;
uint32_t count;
int32_t sum; // 用于快速计算平均值
int32_t max_value;
int32_t min_value;
} sliding_window_t;
void window_push(sliding_window_t *w, int32_t value) {
// 如果窗口满了,先移除最旧的数据
if (w->count == WINDOW_SIZE) {
int32_t old = w->buffer[w->head];
w->sum -= old;
// 如果移除的是最大值或最小值,需要重新扫描
// (这里为了简洁,省略了重新扫描的逻辑)
} else {
w->count++;
}
// 插入新数据
w->buffer[w->head] = value;
w->head = (w->head + 1) % WINDOW_SIZE;
w->sum += value;
// 更新最大最小值
if (value > w->max_value) w->max_value = value;
if (value < w->min_value) w->min_value = value;
}
int32_t window_average(sliding_window_t *w) {
if (w->count == 0) return 0;
return w->sum / w->count;
}
性能提示:窗口越大,维护最大最小值的代价越高。如果窗口很大(比如 1000 个元素),我建议用「双端队列 + 单调栈」来优化,而不是每次重新扫描。
15.4 三种模式如何配合使用
在实际项目中,这三种模式很少单独出现。它们通常串成一条流水线:
- 原始事件 → 先经过过滤器,去掉噪声和无效事件。
- 干净事件 → 进入聚合器,攒够一批或者超时后触发。
- 批量事件 → 送入窗口计算器,做统计或趋势分析。
举个例子,一个工业振动监测系统:
- 传感器每 10ms 上报一次振动幅度。
- 过滤器先去掉超过 ±100g 的异常值(可能是传感器脱落)。
- 聚合器每收到 100 个有效数据(即 1 秒),打包成一个批次。
- 窗口计算器维护最近 10 个批次(即 10 秒)的 RMS 值,判断是否超过报警阈值。
你看,三个模式一组合,一个完整的监测逻辑就出来了。
我的习惯:我会把过滤器、聚合器、窗口计算器都做成「可插拔」的模块。每个模块只依赖事件结构体,不依赖具体业务。这样换一个项目,代码基本不用改,改改配置参数就行。
15.5 一张图看懂事件处理流水线
下面这张图展示了从原始事件到最终输出的完整流程:
15.6 避坑指南与最佳实践
最后,分享几个我踩过的坑:
- 过滤器太严格:我曾经把变化率阈值设得太小,结果正常的数据也被丢了。系统看起来「很安静」,其实已经出问题了。建议先用日志模式跑一段时间,看看过滤掉了多少数据,再调整参数。
- 聚合器忘记超时:前面说过了,不加超时,事件源一停就死锁。一定要加,而且超时时间要合理——太短会频繁触发,太长会延迟响应。
- 窗口计算的内存占用:如果窗口很大(比如 10000 个元素),每个元素是 4 字节,那就是 40KB。在资源受限的 MCU 上,这可能直接撑爆 RAM。我建议用环形缓冲区 + 分段统计来降低内存需求。
- 优先级反转:如果过滤器和聚合器运行在不同的任务中,要注意优先级。过滤器通常跑在中断或高优先级任务里,聚合器可以跑在低优先级任务里。别让聚合器阻塞了过滤器的处理。
总结一下:事件过滤与聚合,说白了就是「去粗取精」和「化零为整」。过滤器帮你挡住垃圾,聚合器帮你攒够弹药,窗口计算帮你看出趋势。三个模式配合好,你的事件处理系统就会既高效又可靠。
嗯,今天就聊到这里。这些模式我在多个项目里反复用过,每次都能让代码结构清晰不少。你可以在自己的项目里试试,从最简单的过滤器开始,慢慢加上聚合和窗口计算,你会发现事件处理变得特别顺手。