最轻量事件循环需用std::queue+std::thread+std::mutex+std::condition_variable,避免空转、确保可移动任务、用std::atomic_bool控制退出;延迟任务用优先队列按时间排序,异常需在循环内捕获,对象生命周期须由shared_ptr或明确作用域保障。

用 std::queue + std::thread 搭一个可投递的事件循环
最轻量、无第三方依赖的起点就是手动维护一个任务队列和一个专用线程。它不处理 I/O,也不做定时调度,但能让你立刻跑通「主线程发任务、后台线程串行执行」这个核心链路。
常见错误现象:任务投递后没执行、程序卡死、多线程崩溃 —— 基本都是因为没加锁或没处理空队列等待。
-
std::queue本身不是线程安全的,post()和run()必须用std::mutex保护 - 空队列时不能
while(true) { /* 空转 */ },要用std::condition_variable::wait_for()或带超时的sleep_for避免 CPU 占满 - 任务执行体(
std::function<void></void>)必须是可移动的,否则queue.push(std::move(task))会失败 - 退出机制不能靠
break,得用std::atomic_bool running_控制循环,否则线程无法安全终止
为什么不用 Boost.Asio 的 io_context?
如果你只需要跨线程投递回调、不做网络或文件 I/O,io_context 是杀鸡用牛刀。它自带调度器、内存池、strand 序列化等一整套机制,启动成本高、调试信息冗长、链接体积大。
典型踩坑点:
立即学习“C++免费学习笔记(深入)”;
- 误以为
io_context::run()是万能事件循环 —— 实际上它默认只响应post()和异步操作完成通知,不自动处理定时器或自定义事件 - 忘记调用
io_context::stop()导致线程无法退出,或者调用太早导致后续post()丢失 - 在非
io_context线程里直接调用dispatch()而不是post(),结果回调在调用线程同步执行,失去异步意义
简单场景下,手写事件循环比初始化一个 io_context 少写 20 行代码、少连 3 个库、少理解 5 个概念。
如何支持延迟任务?加一个 std::priority_queue
普通队列只能 FIFO 执行,要实现 post_after(2s, []{...}),就得换优先队列,按触发时间排序。关键不是“怎么堆”,而是“怎么不卡住主循环”。
性能与逻辑要点:
- 优先队列元素必须可比较,推荐用
std::chrono::steady_clock::time_point作主键,避免系统时间跳变影响排序 - 不能每次循环都遍历整个队列查到期任务 —— 要用
top()取堆顶,只检查第一个是否到期;未到期就wait_until到那个时间点 - 延迟任务和即时任务最好分两个队列,否则每次插入都要比较时间戳,且即时任务可能被压在堆底迟迟得不到执行
- 如果任务回调里抛异常,必须在事件循环内捕获(
try { task(); } catch(...) {}),否则整个循环会退出
回调函数里访问外部对象,生命周期谁负责?
这是最隐蔽也最容易崩的一环。比如你写 loop.post([&obj]{ obj.do_something(); });,但 obj 在任务执行前已被析构,就会 crash。
实际做法只有三种可靠路径:
- 用
std::shared_ptr包裹捕获对象,并在 lambda 中按值捕获该智能指针([ptr = shared_from_this()](){ ptr->do_something(); }) - 确保投递任务的对象生命周期严格长于事件循环 —— 比如把
EventLoop成员变量放在类尾部,保证它最后析构 - 完全避免捕获,改用消息结构体传值(
struct Msg { int id; std::string data; };),回调里只处理 POD 数据
没有“自动管理”的银弹。C++ 的事件驱动从不隐藏所有权,你写的每一行捕获代码,都在明确声明谁持有谁的生命周期。



















