正文内容
std::stop_token 解析
std::stop_token 是 C++20 的协作式取消机制。它不强制终止线程,而是由请求方发出停止请求,执行方在合适的位置自行检查并安全退出。
核心组件:
std::stop_source:持有“请求停止”的能力。std::stop_token:可复制的只读观察句柄,交给任务或线程。std::stop_callback:停止请求发出时同步执行的回调。std::jthread:自动传入stop_token,析构时会先请求停止、再join。
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
void worker(std::stop_token stopToken) {
while (!stopToken.stop_requested()) {
// 做一小段可中断工作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "Worker stopped safely.\n";
}
int main() {
std::jthread thread(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
thread.request_stop();
} // jthread 析构:request_stop() + join()
request_stop() 的特点:
- 是幂等操作:第一次通常返回
true,后续调用返回false。 - 请求一旦发出不可撤销。
- 多个
stop_token可以共享同一个停止状态。 - 停止状态由最后一个关联的
stop_source、stop_token、stop_callback释放后销毁。 - 默认构造的
std::stop_token没有关联停止状态,stop_possible()为false。
std::stop_source source;
std::stop_token token = source.get_token();
bool firstRequest = source.request_stop(); // true
bool alreadyRequested = source.request_stop(); // false
if (token.stop_requested()) {
// 执行清理并返回
}
与 std::jthread 配合
当线程函数首个参数能接受 std::stop_token 时,jthread 会自动注入它:
std::jthread thread([](std::stop_token stopToken, int intervalMs) {
while (!stopToken.stop_requested()) {
// ...
}
}, 50);
若线程函数不接受 token,jthread 仍可使用,只是任务无法感知停止请求。
相比 std::thread,std::jthread 通常更适合管理后台工作线程,因为它避免了析构时遗漏 join() 导致 std::terminate() 的问题。不过要注意:若任务永远不检查 token,jthread 析构仍会一直阻塞在 join()。
阻塞等待的正确写法
只在循环中轮询 stop_requested() 不够理想,因为任务可能卡在条件变量、队列或 I/O 等待中。C++20 的 condition_variable_any 支持直接传入 token:
#include <condition_variable>
#include <mutex>
#include <stop_token>
std::mutex mutex;
std::condition_variable_any condition;
bool hasWork = false;
void worker(std::stop_token stopToken) {
std::unique_lock lock(mutex);
condition.wait(lock, stopToken, [] {
return hasWork;
});
if (stopToken.stop_requested()) {
return;
}
// 处理工作
}
这会在停止请求到来时唤醒等待者。注意,这个带 token 的重载是 std::condition_variable_any 提供的,不是 std::condition_variable。
对于不支持 token 的阻塞 API,可注册回调来唤醒等待:
std::stop_callback callback(stopToken, [&] {
condition.notify_all();
});
然后仍需在被唤醒后检查停止状态并退出。回调本身不应执行重活、阻塞操作或尝试获取可能被当前线程持有的锁。
std::stop_callback 的并发语义
这是最容易踩坑的部分:
std::stop_callback onStop(token, [] {
// 收到停止请求时执行
});
- 若停止尚未请求,回调会在另一个线程调用
request_stop()时同步运行。 - 若构造
stop_callback时停止已经被请求,回调会在构造函数中立即、同步执行。 request_stop()会等待正在执行的相关回调完成后才返回。- 因此回调不可抛异常、不可长期阻塞,也要警惕锁顺序导致死锁。
推荐将回调限制为“解除阻塞”:
std::stop_callback onStop(stopToken, [&queue] {
queue.notify_waiters();
});
不推荐在其中直接关闭复杂资源、执行大规模清理或调用未知的用户代码。
推荐的任务函数结构
void processRequests(std::stop_token stopToken) {
try {
while (!stopToken.stop_requested()) {
auto request = waitForRequest(stopToken);
if (!request || stopToken.stop_requested()) {
break;
}
process(*request);
}
} catch (...) {
// 记录错误;必要时通知任务协调器
}
// 在这里执行本线程拥有资源的收尾工作
}
实践原则:
- 在工作单元之间检查
stop_requested(),不要只在最外层检查一次。 - 让每个工作单元足够短,或让内部算法也能接收 token。
- 把 token 作为任务函数的第一个参数,保持与
std::jthread的约定一致。 - 资源清理放在任务正常退出路径中,而不是依赖回调完成。
- 对长时间阻塞的操作,优先选择原生支持取消或超时的 API。
- 取消是“请求”,不是“保证立刻停止”;API 文档应明确最大退出延迟。
常见误区
// 错误:这不是强制中断
source.request_stop();
调用后,线程不会自动消失;任务必须主动响应。
// 风险:任务没有读取 token,jthread 析构可能永远等待
std::jthread thread([] {
for (;;) {
doWork();
}
});
// 风险:回调可能在构造期间同步执行
std::stop_callback callback(token, [&] {
useObjectNotFullyInitialized();
});
应先完成对象初始化,再注册回调,或确保回调可安全处理该时机。
何时使用
适合:
- 后台工作线程、轮询任务、任务队列消费者。
- 可分段计算、批量处理、可取消算法。
- 需要统一关闭协议的服务组件。
不适合直接解决:
- 无法中断的第三方阻塞调用。
- 需要强制终止失控线程的场景。
- 跨进程或跨网络取消协议;这些仍需应用层取消消息或连接关闭策略。
一句话总结:std::stop_token 将线程取消从“共享原子标志的临时约定”升级为标准化、可组合、可唤醒阻塞等待的协作式取消协议;std::jthread 则是其最自然的线程生命周期载体。
std::jthread 讲解
std::jthread 是 C++20 引入的“自动管理生命周期的线程”。它和 std::thread 的主要差别是:
- 析构时自动
join(),不会因忘记join()/detach()而触发std::terminate()。 - 支持协作式停止,内置
std::stop_source与std::stop_token。 - 当线程函数的第一个参数是
std::stop_token时,会自动传入取消令牌。
#include <chrono>
#include <iostream>
#include <thread>
int main() {
std::jthread worker([] {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "任务结束\n";
});
} // 自动 join,确保线程结束后才离开作用域
等价于大致如下的 std::thread 写法:
std::thread worker([] {
// 任务
});
if (worker.joinable()) {
worker.join();
}
但 jthread 用 RAII 自动完成收尾,更不容易遗漏。
配合停止请求
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
void run(std::stop_token token) {
while (!token.stop_requested()) {
std::cout << "working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "收到停止请求,安全退出\n";
}
int main() {
std::jthread worker(run);
std::this_thread::sleep_for(std::chrono::seconds(2));
worker.request_stop();
}
调用关系:
jthread.request_stop()
|
v
内部 stop_source.request_stop()
|
v
线程函数中的 stop_token.stop_requested() == true
|
v
任务自行清理并退出
request_stop() 只是“请求”停止,不会强制杀死线程。线程函数必须定期检测:
while (!token.stop_requested()) {
doOneUnitOfWork();
}
否则 jthread 析构时仍会等待线程完成:
std::jthread worker([] {
while (true) {
doWork(); // 不检查 token,析构可能永久阻塞
}
});
参数传递规则
当首个参数是 std::stop_token 时,jthread 自动注入 token:
void download(std::stop_token token, std::string url);
std::jthread worker(download, "https://example.com/data");
实际调用近似为:
download(worker.get_stop_token(), "https://example.com/data");
如果函数不接受 std::stop_token,也可以正常使用:
std::jthread worker([](int count) {
for (int index = 0; index < count; ++index) {
// ...
}
}, 10);
只是 request_stop() 不会让该线程函数产生实际效果。
适合的场景
- 后台任务、定时轮询、日志写入线程。
- 生产者/消费者任务。
- 服务关闭时需要优雅退出的工作线程。
- 临时并发任务,作用域结束时必须等待完成。
使用建议
- 新代码通常优先选
std::jthread,除非确实需要detach()。 - 将
std::stop_token放在线程函数的第一个参数。 - 将长任务拆成可中断的小步骤,周期性检查
stop_requested()。 - 对条件变量等待,使用
std::condition_variable_any::wait(lock, token, predicate),这样停止请求可唤醒等待。 - 不要在持有关键锁时依赖线程析构,因为析构中的
join()可能等待一个同样需要该锁的线程,造成死锁。
std::mutex mutex;
void worker(std::stop_token token) {
std::unique_lock lock(mutex);
while (!token.stop_requested()) {
// ...
}
}
// 避免:持锁销毁 jthread,worker 可能正等待 mutex
一句话:std::jthread 用 RAII 保证线程会被等待结束,并通过 stop_token 提供标准的优雅取消通道。
正文内容已启用复制保护,代码块与行内代码仍支持复制。
读者留言
还没有评论,来发表第一条评论吧。