std::thread详解
std::thread
thread是个类,它的构造函数是个函数模板,且使用了万能引用,thread维护着一个tuple成员,在构造函数中将推导出的类型用remove_reference去掉引用然后传给tuple这个类模板,这样tuple中保存的就是值类型而没有引用类型,再用推导出的类型给引用变量进行完美转发,然后传给tuple的构造函数,这样tuple中的值就是根据给thread传的实参来进行拷贝构造或移动构造,thread设计一个static成员函数,这个函数是void*(*)(void*)类型,这个成员函数利用void*参数得到tuple,然后auto func=get<0>(move(tuple)) ,tuple以右值传给get,所以tuple中第一个值也就是函数是以右值返回的,auto只有万能引用会推导引用类型,其他都是值,所以移动构造得到func,接着遍历tuple剩下的参数,全部都是给get传参move后的tuple,使tuple中参数都以右值的形式返回,然后传给可调用对象func。thread在构造函数函数体中调用pthread_create,参数传tuple的地址,函数的话传那个成员函数,然后创建新线程,执行成员函数,成员函数利用void*参数得到tuple,从tuple中以右值的形式取出可调用对象,然后再从tuple中以右值的形式把参数再都取出来然后传给可调用对象
关键点:
1、tuple中保存的是值类型,tuple中的元素在构造时使用完美转发
2、tuple中的参数是以右值的形式传给可调用对象的
如何传主线程的值给新线程呢?
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;void func(int& t)
{cout<<t;
}int main()
{int a=10;thread th(func,a);th.join();return 0;
}
来详细分析一把上面代码,func和a都是左值,所以thread构造函数万能引用,推导的都是左值引用类型,然后去掉引用作为tuple的类型模板参数,tuple中的元素都以值形式保存,调用拷贝构造来初始化,后面thread构造函数调用pthread_create,执行thread的成员函数,从void*解析出tuple结构体,然后用get,以右值的形式获取了可调用对象,用auto来接受,上演了一出移动构造的戏码,然后以右值获取其他的tuple中的参数然后传给可调用对象,问题来了,tuple中的int变量以右值的形式返回,但是可调用对象是用左值引用变量来接收,所以会报错
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;
void Print(int n, int& rx, mutex& rmtx)
{rmtx.lock();for (int i = 0; i < n; i++){// t1 t2++rx;}rmtx.unlock();
}int main()
{int x = 0;
mutex mtx;// 这⾥必须要⽤ref()传参,现成中拿到的才是x和mtx的引⽤thread t1(Print, 1000000, ref(x), ref(mtx));
thread t2(Print, 2000000, ref(x), ref(mtx));
t1.join();
t2.join();
cout << x << endl;return 0;
}
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *), void *restrict arg);
如何将主线程的值传给可调用对象呢?第一种策略就是从参数下手,使用ref模板函数,传参x,会创建reference_wrapper,其底层维护一个指针int*,用x去初始化这个指针,然后值返回的形式返回reference_wrapper,tupple中会有reference_wrapper,并以移动构造来初始化,因为值返回会创建临时变量,就是右值,后面使用get函数从tuple中以右值形式返回reference_wrapper,左值引用变量接收,自动执行operator type&成员函数,解引用维护的指针并以左值形式返回对象,然后左值引用变量就可以接受了,获取地址
void Print(int n, int& rx, mutex& rmtx)
{rmtx.lock();for (int i = 0; i < n; i++){// t1 t2++rx;}rmtx.unlock();
}int main()
{int x = 0;
mutex mtx;// 将上⾯的代码改成使⽤lambda捕获外层的对象,也就可以不⽤传参数,间接解决了上⾯的问
题auto Print = [&x, &mtx](size_t n) {mtx.lock();for (size_t i = 0; i < n; i++){++x;}mtx.unlock();
};thread t1(Print, 1000000);
thread t2(Print, 2000000);
t1.join();
t2.join();
cout << x << endl;return 0;
}
将主线程的变量传给新线程的第二种方法就是从函数下手,直接让处理函数绑定一些参数,或者是像lambda表达式这样捕获一些变量,直接省了很多逻辑,更好理解了
remove_reference、remove_const
1、remove_reference
在thread的原理中涉及了类型的去引用化,就可以使用remove_reference来实现
template <class T>
struct remove_reference {using type = T; // 默认情况:T 不是引用,直接保留原类型
};template <class T>
struct remove_reference<T&> { // 特化处理左值引用using type = T;
};template <class T>
struct remove_reference<T&&> { // 特化处理右值引用using type = T;
};
remove_reference是一个模板类,对于模板类的类型模板参数要么显式给、要么缺省给,还有模板特化,以前一直不太清楚模板特化的作用,现在来看模板特化就是特定的类型模板参数,使其特化出特别的类。像remove_reference如果显式给一个普通类型,那就用using给这个普通类型取一个叫type的类型别名,显式给的是左值引用类型,那type就是去了引用后的类型的别名,类型模板参数传右值引用类型同理
#include <type_traits>
#include <iostream>int main() {// 使用 remove_reference::typestd::remove_reference<int>::type a = 10; // intstd::remove_reference<int&>::type b = 20; // intstd::remove_reference<int&&>::type c = 30; // intstd::cout << a << ", " << b << ", " << c << std::endl;
}
2、remove_const
template< class T >
struct remove_const {typedef T type; // 主模板:直接保留原类型
};template< class T >
struct remove_const<const T> {typedef T type; // 特化版本:移除 const 限定符
}
1、模板特化,对于特定的类型模板参数特化出特定的类
2、使用typedef或using给类型取别名,保存在实例化出来的类中
std::tuple和std::get
1、std::tuple
tuple是一个类模板
template <class... Types> class tuple;
需要显式给类型模板参数,然后tuple会根据类型模板参数来创建有名变量,接着就是调用构造函数来初始化这些元素
template <class... UTypes>explicit tuple (UTypes&&... elems);
构造函数是一个函数模板,应用了万能引用,注意不要把构造函数的函数模板和类模板合一块了,这俩各是各的类型模板参数。
构造函数会用万能引用加完美转发美美地将元素都初始化了
2、std::get
(1) | template <size_t I, class... Types> typename tuple_element< I, tuple<Types...> >::type& get(tuple<Types...>& tpl) noexcept; |
---|---|
(2) | template <size_t I, class... Types> typename tuple_element< I, tuple<Types...> >::type&& get(tuple<Types...>&& tpl) noexcept; |
get是函数模板,因为这边非类型模板参数没有设置缺省值,所以需要显式给,其他的类型模板参数都可以由参数的类型推导给得到,这边参数就是tuple,有两种传参方式,一种左值一种右值,这会影响到返回tuple的元素时是以左值形式返回还是右值形式返回,至于非类型模板参数就是tuple第几个元素
注意:get是函数模板,且没使用万能引用,因为不符合格式T&&
std::ref和std::refenrence_wrapper
1、std::ref
ref是函数模板,可以使用左值或const左值来传参,然后ref函数会用推导出的类型模板参数T来构造reference_wrapper对象,其底层维护着一个T*类型的指针,用ref引用变量形参来初始化这个指针。如果传的是左值,那T推导的就是左值,T传给reference_wrapper,于是其底层维护的指针就是左值指针,传的是const左值,那T就是const左值类型,T传给reference_wrapper,其底层维护的指针就是const指针,指向的对象不可变。这个函数会返回一个reference_wrapper对象,值返回的形式,所以返回的是个右值
template <class T> reference_wrapper<T> ref (T& elem) noexcept; |
template <class T> void ref (const T&&) = delete; |
2、std::reference_wrapper
template <class T> class reference_wrapper;
这是一个类模板,底层维护着一个T*的指针
reference_wrapper (type& ref) noexcept;
reference_wrapper (type&&) = delete;
这是其构造函数,利用左值来初始化指针变量成员,可以看到右值的构造直接被禁了,没错,引用包装器只包装左值
reference_wrapper有operator type& 成员函数,会在reference_wrapper强转type&类型时调用,将维护的变量以type&也就是左值形式返回,这也是为什么get从tuple中以右值的形式得到reference_wrapper后,最后自定义可调用对象的引用变量可以成功接收到主线程的变量的原因
提问:为何reference_wrapper不用引用变量来维护左值或const左值呢?
如果reference_wrapper底层是引用变量,引用变量在初始化绑定对象后底层维护的指针就没法改了,所以直接用指针,这样就可以实现reference_wrapper的赋值运算符重载