std::thread直接执行阻塞IO(如read/recv)会使线程挂起、CPU时间片被收回,导致线程资源浪费、吞吐下降;应改用非阻塞IO+epoll或io_uring等异步机制。

为什么 std::thread 直接跑 IO 会卡住整个线程池
IO 阻塞(比如 read()、fgets()、std::cin >> 或网络 recv())会让调用线程彻底挂起,CPU 时间片被系统收回,直到数据就绪。这时候哪怕你开了 8 个 std::thread,只要其中 1 个在等磁盘读或慢网络响应,它就占着一个线程白等——其他任务无法抢占,线程资源被浪费。
常见错误现象:
– 线程数设得很多,但 CPU 使用率始终低于 30%
– 日志显示某线程长时间无输出,gdb attach 后停在 read 或 epoll_wait 系统调用里
– 同一时刻只有少数几个请求能并发处理,吞吐上不去
- 不要在
std::thread的工作函数里直接做阻塞式文件读写或同步 socket 收发 - 即使用了
std::async(std::launch::async, ...),底层仍是普通线程,同样会被 IO 卡死 - Linux 下可临时用
strace -p <pid>看线程是否卡在read/recvfrom等系统调用
用 io_uring 或 epoll + 线程池替代阻塞 IO(Linux)
核心思路:把 IO 操作从“线程等数据”变成“注册事件 → 继续干活 → 数据来了再通知”。需要换掉阻塞 API,改用异步接口。
对磁盘文件:优先走 io_uring(5.1+ 内核),它支持真正的异步文件读写;若内核太老,至少用 O_DIRECT + 线程池预分配 buffer,避免 page cache 锁竞争。
立即学习“C++免费学习笔记(深入)”;
对网络:必须用非阻塞 socket + epoll(或 io_uring)。每个连接设 fcntl(fd, F_SETFL, O_NONBLOCK),然后用 epoll_ctl 注册 EPOLLIN/EPOLLOUT,只在 epoll_wait 返回时才调用 recv/send,且要循环处理直到返回 EAGAIN。
-
epoll本身不耗 CPU,但一个epoll_wait调用可监控上千连接,远比为每个连接开一线程划算 - 别把
epoll_wait放在单个线程里当调度中心——它容易成瓶颈;推荐 1 个主线程epoll分发事件,N 个工作线程处理计算密集型逻辑 -
io_uring的IORING_OP_READV等操作需提前注册 buffer 和 file,首次 setup 开销大,但后续 submit/complete 极快
Windows 上怎么绕过 ReadFile 阻塞
Windows 没有 epoll,但有成熟的重叠 IO(Overlapped IO)机制。关键不是换线程模型,而是换 API 调用方式。
所有涉及文件、管道、socket 的读写,必须用带 LPOVERLAPPED 参数的版本:如 ReadFile、WSARecv、CreateIoCompletionPort 配合 GetQueuedCompletionStatus。这时调用立即返回,实际 IO 在内核完成,结果通过完成端口(IOCP)投递到工作线程。
- 不要用
ReadFile的同步模式(即lpOverlapped == nullptr),那和 Linux 阻塞调用一样卡 - IOCP 线程池大小建议设为
min(4, std::thread::hardware_concurrency()),太多线程反而因上下文切换降低吞吐 - 注意
WSAEventSelect是事件驱动,不如 IOCP 高效;select()在 Windows 上有 64 句柄硬限制,早该弃用
别忽略缓冲区和内存拷贝带来的隐性阻塞
即使用了异步 IO,如果每次收包都 new 一块 buffer,或者频繁 memcpy 到业务结构体,GC 或锁竞争(如 malloc 的 arena lock)也会拖慢整体响应。
典型问题:
– std::vector<char>(4096) 在循环里反复构造析构,触发多次堆分配
– 网络包解析时用 std::string::append 累加,引发多次 realloc
– 多线程共用一个 std::queue<std::shared_ptr<Packet>>,push/pop 时锁争用严重
- 预分配固定大小 ring buffer 或对象池(如
boost::object_pool或自定义 slab allocator)管理 IO buffer - 用
std::string_view接收原始数据,解析时只记录偏移,避免拷贝 - 跨线程传递数据尽量用无锁队列(如
moodycamel::ConcurrentQueue),或按连接 ID 做线程亲和(affinity),减少共享
真正卡住多线程性能的,往往不是“要不要异步”,而是“有没有把阻塞点从系统调用层,一路挖到内存分配和数据结构层”。漏掉任意一层,优化就打折扣。



















