C++线程的唤醒与安全停止
- [1. 前言](#1. 前言)
- [2. 先区分:唤醒不等于停止](#2. 先区分:唤醒不等于停止)
- [3. C++11:condition_variable 实现等待、唤醒和退出](#3. C++11:condition_variable 实现等待、唤醒和退出)
-
- [3.1 为什么 wait 要带判断条件](#3.1 为什么 wait 要带判断条件)
- [4. C++20:jthread 和 stop_token 协作式停止](#4. C++20:jthread 和 stop_token 协作式停止)
- [5. C++20:停止正在等待的线程](#5. C++20:停止正在等待的线程)
- [6. 常见误区](#6. 常见误区)
-
- [6.1 把 notify 当成消息队列](#6.1 把 notify 当成消息队列)
- [6.2 修改共享状态时不加锁](#6.2 修改共享状态时不加锁)
- [6.3 工作期间一直占用互斥量](#6.3 工作期间一直占用互斥量)
- [6.4 认为 request_stop 会立即中断一切](#6.4 认为 request_stop 会立即中断一切)
- [6.5 忘记 std::thread 的生命周期](#6.5 忘记 std::thread 的生命周期)
- [7. 如何选择](#7. 如何选择)
- [8. 一页速查](#8. 一页速查)
- [9. 总结](#9. 总结)
1. 前言
最近遇到线程控制问题时,我才发现"让线程停下来"和"把正在等待的线程唤醒"并不是同一件事。以前很容易把它们理解成直接暂停、恢复或者杀死线程,但在标准 C++ 中,更推荐的方式是:共享状态负责表达意图,条件变量负责唤醒,线程自己在安全的位置退出。
这篇文章记录 C++11 的 condition_variable,以及 C++20 的 jthread、stop_token 和 request_stop 的用法,方便以后遇到类似问题时快速查阅。
2. 先区分:唤醒不等于停止
| 操作 | 解决的问题 | 常见接口 |
|---|---|---|
| 等待 | 暂时没有任务时,避免线程空转 | condition_variable::wait |
| 唤醒 | 状态发生变化,通知等待线程重新检查 | notify_one、notify_all |
| 请求停止 | 告诉线程应该结束工作 | request_stop、stop_token |
| 等待结束 | 等待线程函数真正返回 | join |
notify_one() 只唤醒一个等待者,notify_all() 会唤醒全部等待者。它们不会自动让线程退出,也不会替我们保存"发生过一次通知"这个状态。
3. C++11:condition_variable 实现等待、唤醒和退出
cpp
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex g_mutex;
std::condition_variable g_cv;
bool g_hasTask = false;
bool g_stopping = false;
void worker()
{
while (true)
{
std::unique_lock<std::mutex> lock(g_mutex);
g_cv.wait(lock, [] {
return g_hasTask || g_stopping;
});
if (g_stopping)
{
break;
}
g_hasTask = false;
lock.unlock();
std::cout << "worker: 开始处理任务" << std::endl;
}
std::cout << "worker: 安全退出" << std::endl;
}
int main()
{
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(g_mutex);
g_hasTask = true;
}
g_cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(g_mutex);
g_stopping = true;
}
g_cv.notify_all();
t.join();
return 0;
}
3.1 为什么 wait 要带判断条件
推荐写法:
cpp
g_cv.wait(lock, [] {
return g_hasTask || g_stopping;
});
而不是只写 g_cv.wait(lock)。原因有两个:
- 条件变量允许"虚假唤醒",线程醒来不代表条件一定成立。
- 通知本身不保存状态。如果先 notify,线程之后才开始 wait,就可能错过通知。
真正可靠的是受互斥量保护的状态,而不是通知本身。
4. C++20:jthread 和 stop_token 协作式停止
C++20 引入了 std::jthread。与 std::thread 相比,它的优点是:
- 析构时会自动请求停止并等待线程结束;
- 线程函数可以接收 std::stop_token;
- 外部可调用 request_stop() 发出停止请求;
- 不容易因为忘记 join() 而触发 std::terminate()。
cpp
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
void worker(std::stop_token token)
{
while (!token.stop_requested())
{
std::cout << "worker: 正在运行" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
std::cout << "worker: 收到停止请求" << std::endl;
}
int main()
{
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop();
// jthread 析构时会自动 join
return 0;
}
request_stop() 不会粗暴地杀死线程,它只是把停止状态设置为 true。线程必须主动检查 stop_requested(),然后自行清理资源并返回。
5. C++20:停止正在等待的线程
如果线程正阻塞在普通 condition_variable::wait() 上,仅调用 request_stop() 不能让普通 wait 自动返回。C++20 可以使用 condition_variable_any 支持 stop_token 的重载,把"等待任务"和"等待停止"合并起来。
cpp
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <stop_token>
#include <thread>
std::mutex g_mutex;
std::condition_variable_any g_cv;
bool g_hasTask = false;
void worker(std::stop_token token)
{
std::unique_lock<std::mutex> lock(g_mutex);
while (!token.stop_requested())
{
const bool hasTask = g_cv.wait(lock, token, [] {
return g_hasTask;
});
if (!hasTask)
{
break; // 因停止请求而返回
}
g_hasTask = false;
lock.unlock();
std::cout << "worker: 处理一次任务" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
lock.lock();
}
std::cout << "worker: 已停止" << std::endl;
}
int main()
{
std::jthread t(worker);
{
std::lock_guard<std::mutex> lock(g_mutex);
g_hasTask = true;
}
g_cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop();
return 0;
}
编译时启用 C++20:
bash
g++ main.cpp -std=c++20 -pthread
6. 常见误区
6.1 把 notify 当成消息队列
条件变量只负责通知,不保存任务。任务队列或停止标志仍需要自己维护,并在互斥量保护下读写。
6.2 修改共享状态时不加锁
共享状态可能被多个线程访问,必须正确同步。通常先持锁修改状态,释放锁后再调用 notify_one() 或 notify_all()。
6.3 工作期间一直占用互斥量
从共享队列取出任务后,应尽快释放锁,再执行耗时工作,否则生产者和其他工作线程都会被阻塞。
6.4 认为 request_stop 会立即中断一切
request_stop 是协作式停止。它不能强制中断 sleep_for、普通条件变量等待、阻塞式 I/O 或第三方库中的长时间调用。遇到这些场景,需要使用支持取消的等待接口、设置超时,或由业务代码定期检查停止状态。
6.5 忘记 std::thread 的生命周期
std::thread 析构时如果仍然 joinable,会调用 std::terminate。使用 std::thread 时必须明确 join() 或 detach();通常不要随意 detach。C++20 项目可以优先考虑 std::jthread。
7. 如何选择
| 场景 | 推荐方案 |
|---|---|
| C++11/14/17 项目 | condition_variable + 停止标志 + join |
| C++20 及以上 | jthread + stop_token |
| 同时等待任务和停止 | condition_variable_any::wait(lock, token, pred) |
| 多个消费者只需唤醒一个 | notify_one |
| 所有等待者都要重新判断状态 | notify_all |
8. 一页速查
cpp
// 唤醒一个等待线程
cv.notify_one();
// 唤醒所有等待线程
cv.notify_all();
// 带条件等待,避免虚假唤醒
cv.wait(lock, [] { return ready; });
// C++20 发出停止请求
thread.request_stop();
// 在线程内部检查停止状态
if (token.stop_requested()) {
return;
}
9. 总结
线程控制的核心不是"从外部强行杀死线程",而是让线程能够在正确的时间醒来、看到共享状态,并在安全位置主动退出。
可以记住三句话:
- 状态必须有明确的存储位置,并正确同步。
- notify 只负责唤醒,真正的条件要由谓词判断。
- request_stop 只表达停止意图,线程需要配合退出。
后续将继续整理 call_once、scoped_lock、atomic::wait、future、promise 等平时不一定主动学习,但在工程中非常实用的 C++ 接口。