STL与事件系统:事件驱动、事件循环、事件分发
事件驱动编程,说白了就是「来了消息再干活」。你想想看,传统的程序像流水线,一条指令接一条指令往下走。但现实世界不是这样的——用户什么时候点鼠标?网络数据包什么时候到?你没法预测。所以我们需要一套机制,让程序「等在那里」,有事件来了就处理,没事件就歇着。
我个人习惯把事件系统拆成三个核心部件:事件源、事件循环、事件分发器。今天我们就用STL的容器和算法,手撸一个轻量级的事件框架。嗯,这里要注意,我们不会去跟Boost.Asio或libevent比性能,而是让你理解背后的设计思想。
事件驱动的基本模型
先看一个最简的事件结构体定义:
enum class EventType {
MouseClick,
KeyPress,
Timer,
Custom
};
struct Event {
EventType type;
int x, y; // 鼠标坐标
int keyCode; // 按键码
std::string data; // 自定义数据
std::chrono::steady_clock::time_point timestamp;
};
事件本身就是一个数据包。它携带了「发生了什么」和「相关参数」。我在项目中遇到过一个问题:一开始把时间戳忘了加,结果调试时发现事件处理顺序乱了,根本不知道哪个先到哪个后到。后来我强制每个事件构造时都打上时间戳,问题迎刃而解。
事件循环:用STL容器做调度
事件循环的核心是一个队列。STL的std::queue天然适合做FIFO调度。但实际项目中,我更喜欢用std::deque,因为它支持双端操作——有时候高优先级事件需要插队到前面。
class EventLoop {
std::deque<Event> eventQueue;
std::mutex mtx;
std::condition_variable cv;
bool running = true;
public:
void postEvent(const Event& evt) {
std::lock_guard<std::mutex> lock(mtx);
eventQueue.push_back(evt);
cv.notify_one();
}
void postUrgentEvent(const Event& evt) {
std::lock_guard<std::mutex> lock(mtx);
eventQueue.push_front(evt);
cv.notify_one();
}
void run() {
while (running) {
Event evt;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] {
return !eventQueue.empty() || !running;
});
if (!running) break;
evt = eventQueue.front();
eventQueue.pop_front();
}
dispatch(evt);
}
}
void stop() {
running = false;
cv.notify_all();
}
private:
void dispatch(const Event& evt);
};
你看,这里用了std::condition_variable做等待通知。为什么不用忙等待?我曾经在一个嵌入式项目里试过轮询队列,CPU占用率直接飙到100%,风扇呼呼转。换成条件变量后,空闲时线程完全休眠,CPU占用率降到接近0。这就是STL并发原语的价值。
事件分发:函数对象与多态
分发器要解决的核心问题是:一个事件来了,谁该处理它?
我建议用std::function + std::unordered_map做事件到处理器的映射。这样既灵活又高效:
class EventDispatcher {
using Handler = std::function<void(const Event&)>;
std::unordered_map<EventType, std::vector<Handler>> handlers;
public:
void registerHandler(EventType type, Handler handler) {
handlers[type].push_back(std::move(handler));
}
void dispatch(const Event& evt) {
auto it = handlers.find(evt.type);
if (it != handlers.end()) {
for (const auto& handler : it->second) {
handler(evt);
}
}
}
};
这里有个细节:我用std::vector<Handler>而不是单个处理器。为什么?因为一个鼠标点击事件,可能同时触发UI更新、日志记录、音效播放等多个动作。把它们都注册到同一个事件类型下,分发时依次调用,干净利落。
核心设计原则:事件源和事件处理器之间是松耦合的。事件源只管「扔事件」,处理器只管「接事件」。中间的事件循环和分发器负责协调。这样你改一个处理器,不会影响其他部分。
SVG:事件系统架构图
实战:一个完整的定时器事件示例
光说不练假把式。我们写一个完整的例子,把上面所有部件串起来:
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
#include <deque>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <condition_variable>
// 前面定义的 Event、EventLoop、EventDispatcher 代码放在这里
int main() {
EventLoop loop;
EventDispatcher dispatcher;
// 注册定时器处理器
dispatcher.registerHandler(EventType::Timer, [](const Event& evt) {
std::cout << "[Timer] 触发时间: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
evt.timestamp.time_since_epoch()).count()
<< " ms, 数据: " << evt.data << std::endl;
});
// 注册自定义事件处理器
dispatcher.registerHandler(EventType::Custom, [](const Event& evt) {
std::cout << "[Custom] 收到自定义事件: " << evt.data << std::endl;
});
// 启动事件循环线程
std::thread loopThread([&loop]() {
loop.run();
});
// 模拟产生事件
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Event timerEvt;
timerEvt.type = EventType::Timer;
timerEvt.data = "心跳包 #1";
timerEvt.timestamp = std::chrono::steady_clock::now();
loop.postEvent(timerEvt);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Event customEvt;
customEvt.type = EventType::Custom;
customEvt.data = "用户登录成功";
customEvt.timestamp = std::chrono::steady_clock::now();
loop.postEvent(customEvt);
// 等事件处理完
std::this_thread::sleep_for(std::chrono::milliseconds(200));
loop.stop();
loopThread.join();
return 0;
}
小提示:实际项目中,事件循环通常跑在主线程或专用线程上。如果你在UI框架里做,千万别在事件处理器里做耗时操作——会卡界面。我的做法是:耗时任务扔到线程池,事件处理器只负责「派活」,不负责「干活」。
避坑指南
我曾经在事件系统里踩过一个坑,说出来你可能不信——事件处理器的注册顺序竟然影响了程序行为。排查了半天才发现,两个处理器都修改了同一个全局状态,后执行的覆盖了先执行的。从那以后,我定了一条铁律:事件处理器之间不能有隐式依赖。如果非要依赖,那就显式地定义优先级。
另一个常见问题是事件泄漏。如果你注册了处理器但忘记注销,对象已经析构了,事件一来就调用了野指针。解决方案有两个:
- 使用
std::weak_ptr包装处理器,分发时检查是否过期 - 在对象析构时主动调用
unregisterHandler
我个人倾向于第二种,因为更直观,性能也更好。
总结
事件系统说白了就是「观察者模式」的异步变体。STL给我们提供了deque做队列、unordered_map做分发表、function做回调、condition_variable做同步——这些积木拼在一起,就是一个可用的轻量级事件框架。
你可能会问:为什么不直接用现成的库?我的回答是:理解原理比会用更重要。当你理解了事件循环的本质,再去用Boost.Asio或Qt的事件系统,你会发现它们万变不离其宗。嗯,今天就到这里,希望你能动手把代码跑起来,改一改,玩一玩。