结论:应优先使用Boost.Asio或libevent而非手写epoll,但必须关闭默认缓冲、自管内存生命周期并调优内核参数,否则高性能只是幻觉。

直接说结论:别从 socket + epoll_wait 手写起,优先用 Boost.Asio 或 libevent,但必须亲手改掉默认配置、关掉默认缓冲、自己管好内存生命周期——否则所谓“高性能”只是幻觉。
为什么裸写 epoll 容易翻车
很多人以为“用上 epoll 就高并发了”,结果压测一跑,QPS 上不去、延迟毛刺多、连接数刚过 5000 就开始 accept 失败。根本原因不是代码逻辑错,而是几个默认行为在拖后腿:
-
epoll_wait默认阻塞等待,没设超时就卡死整个事件循环; - 没配
SO_REUSEPORT,多线程 accept 会争抢同一个监听 socket,造成惊群和锁竞争; - 读写缓冲区用
std::string或临时vector<char></char>,每次收发都 new/delete,触发内存碎片+系统调用; - 误用水平触发(LT)模式处理长连接,一个就绪 fd 反复被通知,却没一次性读完(
recv返回EAGAIN没判),导致 CPU 空转。
Boost.Asio 不是开箱即用的银弹
Boost.Asio 确实封装了 epoll/IOCP,但它的默认行为面向通用场景,不是为高吞吐低延迟优化的:
-
io_context::run()单线程调用时,所有异步操作串行执行——哪怕你有 8 核 CPU,也只跑满 1 个核; -
async_read默认用dynamic_buffer,背后是std::vector动态扩容,小包频繁收发时内存抖动明显; -
shared_ptr管理 session 生命周期很安全,但 refcount 原子操作在每连接每请求都发生,百万连接下就是可观的原子指令开销; - SSL 支持默认走 OpenSSL 同步 BIO,会把异步模型拉回阻塞路径,必须显式配
ssl::stream<:socket></:socket>+ 非阻塞 socket。
libevent 的 event_base 必须按线程隔离
很多人想“一个 event_base 多线程共用”,结果出现 event_add: event has no event_base set 或随机崩溃。libevent 的 event_base 不是线程安全的:
立即学习“C++免费学习笔记(深入)”;
- 每个工作线程必须有自己的
event_base实例,不能共享; - 跨线程投递事件要用
event_active+event_base_loopbreak配合evthread_make_request,而不是直接event_add; - 定时器(
event_set_timer)如果注册在非当前线程的event_base上,会被忽略——必须在对应线程的 event loop 里注册; -
bufferevent的读写回调默认在触发它的那个event_base线程执行,业务逻辑重就容易堵住 IO 路径,得手动把耗时操作 offload 到线程池。
真正卡性能的从来不是 epoll_wait
火焰图反复验证过:当连接数超过 1 万,CPU 热点往往不在 epoll_wait,而在三处:
- 内存分配:
new Session/malloc(4096)占比常超 25%,尤其小对象高频创建;解法是预分配连接池 + 固定大小 slab 内存块; - 系统调用链路:
gettimeofday、clock_gettime(CLOCK_MONOTONIC)每次收包都调?缓存到本地变量,每 100 次事件更新一次; - 日志同步刷盘:
std::cout 这种写法在高并发下会锁 <code>std::cout全局流,直接拖垮吞吐——必须用无锁环形队列 + 独立日志线程消费。
最后提醒一句:内核参数不调,再好的代码也白搭。net.core.somaxconn、fs.file-max、net.ipv4.ip_local_port_range 这三个值不提前扩到 65535 以上,accept 队列溢出和端口耗尽问题会在凌晨三点准时找你。



















