C++进阶专栏:http://t.csdnimg.cn/HGkeZ
目录
1.前言
2.std::is_invocable_v
3.std::jthread
3.1.构造函数
3.2.std::jthread无需join/detach使用实例
3.3.std::jthread处理外部请求中断实
3.4.处理中断请求示例代码
4.特性
5.总结
1.前言
C++11以来提供了C++原生的多线程std::thread,这极大的方便了多线程的书写。在此之前书写多线程时需要平台原生API,这对于跨平台尤其是跨多平台程序来讲,多线程部分代码书写及维护都是极大的工作量。std::thread具有非常高的优势,但是其也有自己的缺点,以下代码为例:
void using_thread_with_no_join()
{
std::thread t{[](){
std::cout<<"sub thread xecate, thread id"<<std::this_thread::get_id();
}};
}
运行如上代码时,会出现崩溃,堆栈信息如下:
由如上堆栈信息可知,崩溃原因为std::thread在析构时,如果对象仍为joinable状态,则会触发中断,为避免崩溃需要在std::thread析构器前需要将其置于非joinable状态,即需要主动调用join或detach接口。如果忘记了便会出现如上的崩溃。
C++惯用法之RAII思想: 资源管理-CSDN博客
既然已经有了RAII思想了,那必然是可以通过该思想来解决忘记join或detach导致崩溃的问题。所以std::jthread应运而生。当然std::jthread不止于此。
2.std::is_invocable_v
std::is_invocable是C++17 中引入的一个类型特性(type trait),用于在编译时检查给定的类型是否可以被调用。换句话说,它可以用来检查一个类型(比如函数、函数对象、lambda 表达式等)是否可以作为函数调用操作符进行调用。
具体来说,std::is_invocable模板接受一个函数类型和一组参数类型作为模板参数,并提供一个名为value的静态成员常量,用于表示给定的函数类型是否可以被调用。如果value为true,则表示给定的函数类型可以被调用,否则表示不可调用。
示例如下:
#include <type_traits>
struct Foo {
void operator()(int, int) {}
};
int main() {
// 检查函数是否可调用
static_assert(std::is_invocable_v<decltype(&main), int, char>); // 错误,main不接受int和char作为参数
static_assert(std::is_invocable_v<decltype(main), void>); // 正确,main不接受任何参数
// 检查函数对象是否可调用
static_assert(std::is_invocable_v<Foo, int, int>); // 正确,Foo有一个接受两个int参数的调用操作符
static_assert(!std::is_invocable_v<Foo, double, double>); // 错误,Foo没有接受两个double参数的调用操作符
// 检查lambda是否可调用
auto lambda = [](int a) { return a * 2; };
static_assert(std::is_invocable_v<decltype(lambda), int>); // 正确,lambda接受一个int参数
}
在上面的示例中,std::is_invocable_v
是 std::is_invocable
的一个简化形式,它直接返回 true
或 false
,而不是一个 std::true_type
或 std::false_type
的实例。
这个特性在模板元编程和泛型编程中特别有用,因为它允许你在编译时基于可调用性来做出决策。
3.std::jthread
剖析其源码是了解其机理的最好方法,std::jthread的部分源码整理如下:
#if _HAS_CXX20
class jthread {
public:
using id = thread::id;
using native_handle_type = thread::native_handle_type;
jthread() noexcept : _Impl{}, _Ssource{nostopstate} {}
template <class _Fn, class... _Args, enable_if_t<!is_same_v<remove_cvref_t<_Fn>, jthread>, int> = 0>
_NODISCARD_CTOR explicit jthread(_Fn&& _Fx, _Args&&... _Ax) {
if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>) {
_Impl._Start(_STD forward<_Fn>(_Fx), _Ssource.get_token(), _STD forward<_Args>(_Ax)...);
} else {
_Impl._Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
}
~jthread() {
_Try_cancel_and_join();
}
jthread(const jthread&) = delete;
jthread(jthread&&) noexcept = default;
jthread& operator=(const jthread&) = delete;
jthread& operator=(jthread&& _Other) noexcept {
// note: the standard specifically disallows making self-move-assignment a no-op here
// N4861 [thread.jthread.cons]/13
// Effects: If joinable() is true, calls request_stop() and then join(). Assigns the state
// of x to *this and sets x to a default constructed state.
_Try_cancel_and_join();
_Impl = _STD move(_Other._Impl);
_Ssource = _STD move(_Other._Ssource);
return *this;
}
void swap(jthread& _Other) noexcept {
_Impl.swap(_Other._Impl);
_Ssource.swap(_Other._Ssource);
}
_NODISCARD bool joinable() const noexcept {
return _Impl.joinable();
}
void join() {
_Impl.join();
}
void detach() {
_Impl.detach();
}
_NODISCARD id get_id() const noexcept {
return _Impl.get_id();
}
_NODISCARD stop_source get_stop_source() noexcept {
return _Ssource;
}
_NODISCARD stop_token get_stop_token() const noexcept {
return _Ssource.get_token();
}
bool request_stop() noexcept {
return _Ssource.request_stop();
}
friend void swap(jthread& _Lhs, jthread& _Rhs) noexcept {
_Lhs.swap(_Rhs);
}
_NODISCARD static unsigned int hardware_concurrency() noexcept {
return thread::hardware_concurrency();
}
private:
void _Try_cancel_and_join() noexcept {
if (_Impl.joinable()) {
_Ssource.request_stop();
_Impl.join();
}
}
thread _Impl;
stop_source _Ssource;
};
#endif // _HAS_CXX20
由以上代码可知:
1. 关注其构造函数:jthread不存在拷贝构造函数和拷贝赋值,存在移动构造函数和移动赋值运算符,即jthread不可拷贝但是可以转移。
2. 关注其成员变量_Impl为std::thread类型,即std::jthread采用RAII思想,在构造函数内构造std::thread,但是在其析构函数内判断是否为joinable状态,若其为joinable状态则调用std::thread的join函数,致使std::thread在析构时恒为非joinable,不会触发崩溃。关于此部分功能不再赘述,完全为std::thread的套壳。
3. 关注其成员变量_Ssource为std::stop_source类型,std::stop_source内维护stop_source的状态,其状态为std::_Stop_state,而std::_Stop_state实则是原子变量,通过判断该原子变量的值来处理线程的外部请求中断。
3.1.构造函数
先看一下源码:
template <class _Fn, class... _Args, enable_if_t<!is_same_v<remove_cvref_t<_Fn>, jthread>, int> = 0>
_NODISCARD_CTOR explicit jthread(_Fn&& _Fx, _Args&&... _Ax) {
if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>) {
_Impl._Start(_STD forward<_Fn>(_Fx), _Ssource.get_token(), _STD forward<_Args>(_Ax)...);
} else {
_Impl._Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
}
...
从上面代码可以看出std::jthread的构造函数分为两种情况:
1)用std::is_invocable_v判断可调用对象_Fn的首个参数为std::stop_token,通过_Ssource.get_token()获取自身的std::stop_token传入_Impl._Start函数中,最终传入_Fn当中;之前我一直没有看懂,然后去看std::jthead的源码才恍然大悟。这种情况,那么线程就可以这样定义:
#include <iostream>
#include <thread>
using namespace std::literals::chrono_literals;
void f(std::stop_token stop_token, int value)
{
while (!stop_token.stop_requested())
{
std::cout << value++ << ' ' << std::flush;
std::this_thread::sleep_for(200ms);
}
std::cout << std::endl;
}
int main()
{
std::jthread thread(f, 5); // 打印 5 6 7 8... 约 3 秒
std::this_thread::sleep_for(3s);
// jthread 的析构函数调用 request_stop() 和 join()。
}
上面的std::jthread构造传入的 f 满足std::is_invocable,于是进入_Impl._Start(_STD forward<_Fn>(_Fx), _Ssource.get_token(), _STD forward<_Args>(_Ax)...); 开启线程。
2)和上面相反的可调用对象( 普通函数、类成员函数、仿函数、lambda函数等等) 首个参数不是std::stop_token,这种情况非常普通,用的也比较多,如下面示例:
#include<thread>
void func(int i,std::jthread& th){
while (th.get_stop_token().stop_requested()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void main() {
std::jthread t1;
t1 = std::jthread(func, 12,std::ref(t1));
// 终止线程的运行
t1.request_stop();
}
线程函数func检查线程对象的令牌状态,主线程通过改变线程对象的令牌状态,来终止子线程的任务。
std::jthread的析构函数调用了join(),所以这里我们不需要显示调用join()来等待。
3.2.std::jthread无需join/detach使用实例
std::jthread j{[]{
std::cout << "sub jthread execuate, thread id" << std::this_thread::get_id();
}};
//正常输出,并未崩溃,某次执行结果如下
//sub jthread execuate, thread id35732
3.3.std::jthread处理外部请求中断实
std::jthread提供三个接口并配合std::stop_token的stop_requested来实现外部请求中段处理。
//std::jthread
_NODISCARD stop_source get_stop_source() noexcept {
return _Ssource;
}
_NODISCARD stop_token get_stop_token() const noexcept {
return _Ssource.get_token();
}
bool request_stop() noexcept {
return _Ssource.request_stop();
}
//stop token
_NODISCARD bool stop_requested() const noexcept {
const auto _Local = _State;
return _Local != nullptr && _Local->_Stop_requested();
}
3.4.处理中断请求示例代码
void using_jthread_with_stop_token()
{
std::jthread j{ [](std::stop_token token) {
std::cout << "sub jthread execate, thread id" << std::this_thread::get_id()<<"\n";
for (int i =0; i< 20; i++)
{
std::cout<<"sub jthread "<<i<<"\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
if (token.stop_requested())
{
std::cout<<"exit sub jthread " << std::this_thread::get_id() << "\n";
return;
}
}
} };
std::cout << "running main thread "<<std::this_thread::get_id()<<"\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
j.request_stop();
std::cout << "exit main thread " << std::this_thread::get_id() << "\n";
}
//output result:
/*
running main thread 34396
sub jthread execate, thread id21536
sub jthread 0
sub jthread 1
sub jthread 2
sub jthread 3
sub jthread 4
exit main thread 34396
exit sub jthread 21536
*/
由源码可知,除直接使用std::jthread对象请求中断外,还可以使用source,即通过std::jthread的get_stop_source接口获得其source,而后通过source来请求中断,示例代码如下:
void using_jthread_with_source_request_stop()
{
std::jthread j{ [](std::stop_token token) {
std::cout << "sub jthread execuate, thread id " << std::this_thread::get_id() << "\n";
for (int i = 0; i < 20; i++)
{
std::cout << "sub jthread " << i << "\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
if (token.stop_requested())
{
std::cout << "exit sub jthread " << std::this_thread::get_id() << "\n";
return;
}
}
} };
auto source = j.get_stop_source();
std::thread t{[](std::stop_source source){
std::cout << "running t thread " << std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
source.request_stop();
},source};
t.join();
std::cout << "t thread joined" << "\n";
}
//output result:
/*
running t thread 20280
sub jthread execuate, thread id 4164
sub jthread 0
sub jthread 1
sub jthread 2
sub jthread 3
sub jthread 4
t thread joined
exit sub jthread 4164
*/
4.特性
std::jthread 是 C++20 中引入的一个新特性,它是 std::thread 的一个扩展,专为与 C++ 的执行策略(execution policies)和并行算法(parallel algorithms)配合使用而设计。std::jthread 的主要目的是提供一种机制,使得线程可以自动地与执行策略一起工作,并在适当的时候进行调度和管理。
std::jthread 提供了以下特性:
自动管理:std::jthread 在其析构时会自动调用 std::jthread::join(),从而避免了忘记调用 join() 或 detach() 而导致的资源泄露或程序行为不确定的问题。
异常传播:如果 std::jthread 运行的函数抛出了异常,并且这个异常没有被捕获,那么 std::jthread 的析构函数会重新抛出这个异常。这使得在 std::jthread 对象的生命周期结束时,能够更容易地诊断和处理异常。
执行策略集成:std::jthread 可以与 C++ 的执行策略(如 std::execution::par、std::execution::seq 等)一起使用,以控制并行算法的执行方式。这使得线程能够更容易地集成到并行计算框架中。
合作式取消:std::jthread 支持一种称为“合作式取消”的机制,允许在适当的时候请求线程停止执行。虽然这并不能强制线程立即停止,但它提供了一种机制,使得线程可以在检查取消请求时优雅地停止。
5.总结
1)std::jthread析构自动汇合,不回崩溃。
2)std::jthread支持joinable、join、detach、get_id、hardware_concurrency等原生std::thread的接口,故std::jthread可以无缝替换std::thread。
3)std::jthread支持外部请求中断,无需再向使用std::thread那样,提供一个标志位来作为线程启停的标志。
参考:
std::jthread - cppreference.com
std::thread - cppreference.com