std::stop_token 和 std::jthread
一直以来,我们在使用 std::thread 时,需要手动管理线程的 join() 或 detach(),并且不能直接支持外部请求的中止,这带来了额外的性能开销和编程复杂性。根据 C++ 长期以来的零开销设计哲学,C++20 引入了 std::jthread 和 std::stop_token。这些新特性不仅自动处理线程的加入,还支持协作式的线程取消,极大简化了线程的使用和管理,而不增加任何未使用功能的成本。 让我们深入了解一下它们的使用和设计哲学。
std::jthread 和 std::stop_token
std::jthread
std::jthread 是 C++20 引入的一个新的线程类,它与 std::thread 类似,但提供了一些重要的改进:
- 自动管理生命周期 :
std::jthread在作用域结束时会自动调用join,因此不需要显式地调用join或detach。 - 内置停止机制 :
std::jthread与std::stop_token集成,支持直接请求停止线程。
- 成员类型
id:std::thread::idnative_handle_type(可选):std::thread::native_handle_type
- 成员函数
- 构造函数: 构造新的jthread对象
- 析构函数: 如果线程是可加入的,请求停止并加入线程
- operator=: 移动jthread对象
- joinable: 检查线程是否可加入,即可能在并行上下文中运行
- get_id: 返回线程的id
- native_handle: 返回底层的实现定义线程句柄
- hardware_concurrency [静态]: 返回实现支持的并发线程数
- 操作
- join: 等待线程完成执行
- detach: 允许线程独立于线程句柄执行
- swap: 交换两个jthread对象
- 停止令牌处理
- get_stop_source: 返回与线程的共享停止状态关联的stop_source对象
- get_stop_token: 返回与线程的共享停止状态关联的stop_token
- request_stop: 通过共享停止状态请求执行停止
我们可以看到 std::jthread支持所有std::thread的功能并提供了扩展。在自动处理线程生命周期的同时,仍提供join和detach方法,保证使用的灵活性和控制。这样设计既维持了与std::thread的接口一致性,方便开发者使用和过渡。
std::stop_token
下是一个关于 std::stop_token 及其相关类的功能和用途的表格:
| 类名 | 说明 |
|---|---|
std::stop_source |
用于发起停止请求的对象。它负责生成 std::stop_token,并在需要时发出停止信号。 |
std::stop_token |
由 std::stop_source 创建的令牌,用于在线程中检测是否有停止请求。可以传递给线程,使其能定期检查并决定是否停止执行。 |
std::stop_callback |
允许注册回调函数,这些函数在 std::stop_source 发出停止请求时被调用,提供一种响应停止请求的机制。 |
共同工作机制
- 创建并发起取消请求 :
std::stop_source对象允许发出取消信号。它可以独立于任何线程存在,并在创建时不自动发起取消请求。 - 生成停止令牌 :
std::stop_source对象可以生成std::stop_token,这些令牌可以被多个接收者共享和检查,以确定是否已经请求停止执行。 - 传递停止令牌 :将
std::stop_token传递给需要支持取消操作的线程或任务。在 C++20 中,std::jthread自动管理线程的生命周期,并接收一个std::stop_token作为其运行函数的参数,这简化了令牌的传递和管理。 - 定期检查停止请求 :在长时间运行或循环任务中,应定期调用
std::stop_token::stop_requested()来检查是否有停止请求。如果返回true,则表示应当尽快安全地停止执行。 - 响应取消请求 :线程或任务应根据
std::stop_token的检查结果决定是否退出。这通常涉及清理资源、保存状态等安全退出的步骤。
示例代码
c++
#include <iostream>
#include <chrono>
#include <thread>
#include <stop_token>
// 使用 std::jthread 运行的函数
void task(std::stop_token stoken) {
// 4. 定期检查停止请求
while (!stoken.stop_requested()) {
std::cout << "任务正在运行..." << std::endl;
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 5. 响应取消请求
std::cout << "任务已收到停止请求,现在停止运行。" << std::endl;
}
int main() {
// 1. 创建并发起取消请求的源
std::stop_source stopSource;
// 2. 生成停止令牌
std::stop_token stoken = stopSource.get_token();
// 3. 传递停止令牌
std::jthread worker(task, stoken);
// 模拟主线程运行一段时间后需要停止子线程
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "主线程请求停止子线程..." << std::endl;
// 触发停止请求
stopSource.request_stop();
// std::jthread 在析构时自动加入
return 0;
}
底层实现机制
std::jthread 在底层实现中利用了 std::thread 的功能,但添加了对线程取消的支持。std::jthread 的主要特点是它在析构时会自动执行线程的加入(join),并且支持协作式取消。这意味着,如果线程函数接受 std::stop_token 作为参数,std::jthread 会自动从其内部的 std::stop_source 获取一个 std::stop_token 并传递给线程函数,从而使得线程可以在适当的时候响应取消请求。 std::stop_source 和 std::stop_token 提供了一种管理和查询停止状态的机制。std::stop_source 负责发起停止请求,而 std::stop_token 允许在关联的 std::stop_source 发起停止请求后进行查询。当 std::stop_source 调用 request_stop() 时,它会修改内部状态以表示停止已被请求,并通过与之关联的所有 std::stop_token 反映这一状态变更。 这种设计使得 std::jthread 和其协作的取消机制不仅简化了线程的使用,还增强了线程间的协作和通信能力,提高了代码的安全性和可维护性。
std::stop_token 和 std::stop_callback的其他使用案例;
另外, std::stop_token 和 std::stop_callback 并不局限于与线程(如 std::jthread)的使用,它们独立于线程的,用于程序中的任何地方,以提供一种灵活的停止信号处理机制。
c++
#include <iostream>
#include <chrono>
#include <stop_token>
int main() {
std::stop_source source;
std::stop_token token = source.get_token();
// 模拟一些可以被取消的工作
auto startTime = std::chrono::steady_clock::now();
auto endTime = startTime + std::chrono::seconds(10); // 设定10秒后结束任务
while (std::chrono::steady_clock::now() < endTime) {
if (token.stop_requested()) {
std::cout << "Task was canceled!" << std::endl;
break;
}
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
// 模拟在某个条件下请求停止
if (std::chrono::steady_clock::now() > startTime + std::chrono::seconds(5)) {
source.request_stop();
}
}
if (!token.stop_requested()) {
std::cout << "Task completed normally." << std::endl;
}
return 0;
}
在这个示例中,我们在主线程中运行一个循环,并通过 std::stop_source 来控制何时应该停止循环。这种方式非常适合于不需要多线程但需要响应取消请求的场景。
它们也可以与 std::thread 结合使用,但使用方式略有不同,因为 std::thread 没有内建的支持来直接接受 std::stop_token。下面是如何在 std::thread 中使用
c++
#include <iostream>
#include <thread>
#include <stop_token>
void threadFunction(std::stop_token stopToken) {
std::stop_callback cb(stopToken, [](){
std::cout << "Thread stopping as requested." << std::endl;
});
while (!stopToken.stop_requested()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread is running..." << std::endl;
}
}
int main() {
std::jthread t(threadFunction);
std::this_thread::sleep_for(std::chrono::seconds(5));
t.request_stop(); // Request to stop the thread.
t.join();
return 0;
}
在这个示例中,std::jthread 在构造时自动接收一个 std::stop_token,该令牌被传递到 threadFunction。在 threadFunction 中,我们注册了一个 std::stop_callback,当外部请求停止线程时,这个回调会被触发,输出一条消息并检查 stop_requested() 来优雅地停止线程。
如果你确实需要在 std::thread 中使用 std::stop_token,你可能需要手动管理这种机制,比如通过线程之间共享的 std::atomic<bool> 或通过其他同步手段来传递停止信号。但通常来说,使用 std::jthread 更为方便和安全。
总结
总的来说,std::jthread 解决 std::thread 缺乏自动资源管理(RAII)的问题,它自动管理线程的生命周期,避免了资源泄漏和死锁的风险,并通过 std::stop_token增加了能够主动取消或停止线程执行的新特性。