16. 事件驱动架构模式:发布-订阅模式、观察者模式、反应器模式、前摄器模式

聊到事件驱动架构,很多初学者容易把几个模式搞混。我当年刚接触嵌入式系统时,也踩过这个坑。说白了,这些模式都是在解决同一个问题——如何让系统对“发生的事”做出响应。但它们的侧重点和适用场景完全不同。

今天咱们就把这四个模式掰开揉碎了讲清楚。我会结合自己实际项目中的经验,告诉你什么时候该用哪个,什么时候千万别用哪个。

核心观点:事件驱动架构的本质,是让系统的控制流由外部事件来驱动,而不是由程序主动轮询。这能极大降低模块间的耦合度。

事件驱动架构四大模式 事件驱动 架构 发布-订阅模式 Pub-Sub 观察者模式 Observer 反应器模式 Reactor 前摄器模式 Proactor 同步 vs 异步 · 主动 vs 被动 · 一对多 vs 多对多

16.1 发布-订阅模式(Pub-Sub)

这个模式我用的最多。它的核心思想是:发布者不直接发送消息给订阅者,而是通过一个“消息代理”来中转。发布者和订阅者之间完全解耦,互相不知道对方的存在。

我在做一个物联网网关项目时,设备上报的数据需要同时送给日志模块、存储模块和告警模块。如果用传统方式,每个新模块加入都要改发送端的代码。后来改用发布-订阅模式,发送端只管往主题里扔数据,谁想订阅谁自己注册,清爽多了。

// 简化的发布-订阅实现
typedef struct {
    char topic[32];
    void (*handler)(void* data);
    struct Subscriber* next;
} Subscriber;

typedef struct {
    char topic[32];
    Subscriber* head;
} TopicNode;

// 发布消息到指定主题
void publish(const char* topic, void* data) {
    TopicNode* node = find_topic(topic);
    if (!node) return;
    
    Subscriber* sub = node->head;
    while (sub) {
        sub->handler(data);  // 通知所有订阅者
        sub = sub->next;
    }
}

// 订阅主题
void subscribe(const char* topic, void (*handler)(void*)) {
    // 注册回调函数到主题列表
}

我的经验:在资源受限的MCU上,不要用动态内存分配来管理订阅者列表。我习惯用静态数组+位图的方式,效率高且可预测。

16.2 观察者模式(Observer)

观察者模式和发布-订阅很像,但有个关键区别:观察者模式中,被观察对象(Subject)直接持有观察者的引用。没有中间的消息代理。

说白了,发布-订阅是“通过中介找房子”,观察者是“直接盯着房东看”。

我举个例子你就明白了。在嵌入式GUI系统中,一个温度数值变化了,需要更新数字显示、进度条和曲线图。这时候用观察者模式就很自然——温度数据对象直接通知所有注册的观察者。

// 观察者模式典型实现
typedef struct Observer {
    void (*update)(int new_value);
    struct Observer* next;
} Observer;

typedef struct {
    int value;
    Observer* observers;
} Subject;

void subject_notify(Subject* sub) {
    Observer* obs = sub->observers;
    while (obs) {
        obs->update(sub->value);
        obs = obs->next;
    }
}

注意:观察者模式容易造成循环依赖。我曾经在一个项目中,A观察B,B又观察A,结果更新时死循环了。后来我加了一个“正在更新”的标志位来防止重入。

16.3 反应器模式(Reactor)

反应器模式是处理I/O多路复用的利器。它的核心是:一个事件循环(Event Loop)不断检查哪些文件描述符就绪了,然后调用对应的处理函数

你想想看,在嵌入式网络服务器中,同时要处理多个客户端的连接请求、数据收发。如果用多线程,资源开销太大。反应器模式用一个线程就能搞定。

// 反应器模式核心:事件循环
void reactor_run() {
    while (1) {
        // 使用select/poll/epoll等待事件
        int ready = poll(fds, nfds, timeout);
        
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) {
                // 调用注册的事件处理器
                handlers[i].on_readable(fds[i].fd);
            }
            if (fds[i].revents & POLLOUT) {
                handlers[i].on_writable(fds[i].fd);
            }
        }
    }
}

我记得在做一个智能家居网关时,需要同时处理Zigbee、WiFi和串口的数据。用反应器模式,一个事件循环就搞定了所有外设的I/O。代码量比用多线程少了至少一半。

16.4 前摄器模式(Proactor)

前摄器模式和反应器模式是“镜像关系”。反应器是“我准备好了,你来读”,前摄器是“你读完了,告诉我结果”

说白了,反应器是同步非阻塞,前摄器是异步非阻塞。前摄器模式中,操作系统或者底层框架帮你完成了I/O操作,然后通过回调通知你。

在嵌入式Linux中,使用POSIX AIO(异步I/O)就是典型的前摄器模式。我曾在视频采集项目中用过这个模式——采集卡采集完一帧图像后,通过信号或回调通知应用层处理,应用层不用干等着。

// 前摄器模式示意:异步读操作
void start_async_read(int fd, void* buf, size_t len) {
    struct aiocb* cb = malloc(sizeof(struct aiocb));
    cb->aio_fildes = fd;
    cb->aio_buf = buf;
    cb->aio_nbytes = len;
    cb->aio_sigevent.sigev_notify = SIGEV_THREAD;
    cb->aio_sigevent.sigev_notify_function = read_complete_handler;
    
    aio_read(cb);  // 发起异步读,立即返回
}

// 读完成后自动调用
void read_complete_handler(sigval_t sigval) {
    struct aiocb* cb = sigval.sival_ptr;
    // 处理读取完成的数据
    process_data(cb->aio_buf);
}

选型建议:

  • 模块间解耦、一对多通知 → 发布-订阅
  • 对象状态变化、一对一或少量观察者 → 观察者模式
  • I/O多路复用、单线程处理多连接 → 反应器模式
  • 高性能异步I/O、不想阻塞主线程 → 前摄器模式

16.5 四种模式对比

特性 发布-订阅 观察者 反应器 前摄器
耦合度 完全解耦 松耦合 中等 中等
事件传递 通过代理 直接通知 事件循环 异步回调
适用场景 系统级消息 GUI更新 网络服务器 高性能I/O
资源开销 中等
实现复杂度 中等 简单 中等 复杂

避坑指南:我曾经在一个项目中强行用前摄器模式处理简单的按键事件,结果异步回调的上下文切换开销比按键处理本身还大。小项目别用大炮打蚊子,合适才是最好的。

嗯,这四个模式讲完了。其实在实际项目中,它们经常混合使用。比如反应器的事件循环里,收到网络消息后通过发布-订阅分发给各个业务模块。灵活组合,才是架构师的功力所在。