必须每次循环检查退出标志,否则线程可能延迟退出;正确写法是while(!stop_requested.load()) {...},阻塞时需将标志嵌入condition_variable谓词并调用notify_all()。

std::atomic 退出标志必须每次循环检查
很多人把 stop_requested 声明成 std::atomic<bool></bool> 就以为万事大吉,结果线程卡在循环里不退出。关键不是“设了标志”,而是线程函数里必须在**每个逻辑周期的入口或关键阻塞点之后**调用 load() 检查。写成 while (true) { /* work */ if (stop_requested.load()) break; } 是危险的——如果 work 耗时很长(比如一次数据库查询、一次网络收包),那退出信号就可能被延迟几十毫秒甚至几秒。
正确做法是:把循环条件直接绑定到原子变量上,例如:
while (!stop_requested.load(std::memory_order_acquire)) {
do_some_work();
std::this_thread::sleep_for(10ms); // 或其他自然让出 CPU 的点
}
这样只要主线程调用 stop_requested.store(true, std::memory_order_release),工作线程下一次迭代就会退出。
阻塞在 condition_variable 上时怎么及时唤醒
线程如果正卡在 cv.wait(lock),它不会主动感知 stop_requested 变化。你不能靠等超时再检查——那样响应太慢。必须把退出条件“塞进谓词”里:
- 用带谓词的
cv.wait(lock, [&]{ return stop_requested.load() || !task_queue.empty(); }),而不是无谓词版本 - 唤醒后不用再手动
if (stop_requested) break,因为谓词已确保退出条件成立才返回 - 主线程发完
stop_requested.store(true)后,**必须紧接着调用cv.notify_all()**,否则等待线程永远收不到信号
漏掉 notify_all() 是线上最常见导致服务 hang 死的原因之一。
线程池 shutdown() 必须先停投递再等执行完
对线程池这类复合结构,shutdown() 不是简单设个标志就完事。顺序错一步,就可能丢任务或死锁:
- 第一步:原子地置
stop = true,并禁止新任务入队(比如把tasks.push()改成直接丢弃或返回 false) - 第二步:调用
cv.notify_all()唤醒所有空闲 worker - 第三步:对每个 worker 线程调用
join()——但注意:join 前要确保它们已从任务循环中退出,否则会永久阻塞
如果线程池还持有连接池、定时器管理器等子系统,shutdown 顺序更要严格:先停新请求接入 → 再 drain 已排队任务 → 最后关闭连接池和定时器。反着来容易触发析构时访问已释放资源。
信号处理与 atexit() 不适用于优雅退出
收到 SIGINT 或 SIGTERM 时,用 signal() 注册 handler 然后调 exit() 是错的——exit() 会跳过线程局部对象析构,且不等待工作线程结束。同样,atexit() 注册的函数只在主线程 exit 时运行,对其他线程完全无效。
真正该做的是:
- 在 signal handler 里只做一件事:
stop_requested.store(true)(这是异步信号安全的操作) - 让主线程在主循环里定期检查该标志,或通过
signalfd()(Linux)/WSAEventSelect()(Windows)把信号转为可 poll 的事件 - 所有清理逻辑(如关闭 listen socket、drain 连接、flush 日志)必须放在工作线程自己的退出路径里,靠 RAII 保证执行
最易忽略的一点:如果你用了自定义线程栈或替换 malloc(比如 jemalloc),确保其 shutdown 钩子也注册到了主线程退出流程中——否则进程看似退出了,后台线程还在默默泄漏内存。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











