直接裸用 std::thread 高并发易崩溃,因无连接复用和请求队列,每连接新建线程致 oom 或调度雪崩;须配合 epoll/iocp、非阻塞 socket、weak_ptr 生命周期管理及 steady_clock 超时。

为什么 std::thread 直接裸用会崩得很快
因为 C++ 标准库不提供线程池、连接复用或请求队列,std::thread 每 accept 一个连接就 new 一个线程,QPS 上去后不是 OOM 就是调度雪崩。Linux 默认每个进程线程数上限常是 1024,pthread_create 失败时返回 EAGAIN,但很多新手没检查就直接 deref 空指针。
- 别在
accept()回调里无限制new std::thread,哪怕加了join()也扛不住短连接洪峰 - 用
std::thread::hardware_concurrency()当线程数上限参考,但实际要更保守(比如设为 CPU 核数 × 2) - 必须检查
std::thread构造是否抛异常——std::system_errorwithstd::errc::resource_unavailable_try_again是真实信号,不是“偶尔出错” - 线程栈默认可能 8MB,
std::thread对象本身不占大内存,但栈空间会快速吃光RLIMIT_STACK
epoll + 非阻塞 socket 是绕不开的起点
Windows 上用 IOCP,Linux 就得靠 epoll_wait() 配合非阻塞 socket,否则一个慢请求卡住整个线程。同步 read/write 在高并发下等于主动放弃吞吐量。
-
socket()后立刻fcntl(fd, F_SETFL, O_NONBLOCK),别等第一次recv()返回EAGAIN才想起来设 -
epoll_ctl()加事件前确认 fd 已设为非阻塞,否则epoll_wait()可能永远不返回(尤其对 pipe 或 eventfd) -
EPOLLET(边缘触发)比EPOLLIN(水平触发)省系统调用,但要求每次recv()必须循环到EAGAIN,否则会漏数据 - 不要在
epoll_wait()返回后对同一个 fd 多次recv()却不检查返回值——recv()返回 0 表示对端 close,-1 且errno == EAGAIN才是正常
std::shared_ptr + weak_ptr 管理连接生命周期容易 double-free
异步模型里,连接对象可能被 I/O 回调、超时器、业务逻辑三处同时持有,shared_ptr 看似安全,但一旦某处忘了用 weak_ptr.lock() 就直接访问,或者析构顺序不对(比如 event loop 先于 connection 对象销毁),就会 crash。
- 所有跨线程持有的连接句柄,必须用
weak_ptr<connection></connection>,回调开头第一行就是auto conn = wp.lock(); if (!conn) return; - 别在
Connection析构函数里调用epoll_ctl(EPOLLDEL)——此时 event loop 可能已停,fd 无效或被复用 -
shared_ptr的控制块分配本身有锁开销,高频创建销毁连接时,考虑用对象池(如boost::object_pool或自定义 arena)预分配 - 用 AddressSanitizer + ThreadSanitizer 编译,
shared_ptr生命周期问题在压测前就能暴露,比线上 core dump 好查得多
std::chrono::steady_clock 是唯一靠谱的超时依据
用 time() 或 std::chrono::system_clock 做超时,NTP 调时、系统休眠、时钟跳跃都会导致连接提前断或死等。C++11 之后没有理由再碰 gettimeofday()。
立即学习“C++免费学习笔记(深入)”;
- 所有定时器(空闲超时、读写超时、心跳间隔)都基于
std::chrono::steady_clock::now(),它不随系统时间调整而跳变 - 别把
steady_clock::time_point存成long long微秒再算差值——精度丢失,直接用duration_cast<milliseconds>(tp - start)</milliseconds> - Linux 的
timerfd_create()可以和epoll集成,但注意它的超时时间是相对当前内核时间,不是你代码里steady_clock::now()那一刻,两者存在微小偏差,关键业务需补偿 - 测试时用
clock_settime(CLOCK_MONOTONIC, ...)强制改系统单调时钟?不行——CLOCK_MONOTONIC不可写,这是故意设计的
真正难的不是写完 epoll 循环,而是让每个连接的 socket、buffer、timer、业务状态在任意时刻被任意线程访问时,既不竞态也不泄漏。这些点没卡死,性能数字再好看也是假的。











