本章的讲解是围绕thread的接口进行的,下面是thread的接口定义:
class thread{ public: class id; //声明 //construction an destruction //**一、构造 thread() noexcept; ~thread(); template<typename Callable, typename Args...> explicit thread(Callable&& func, Args&&...args); //copy and move //**二、拷贝、赋值 thread(const thread& other) = delete; thread(thread&& other) noexcept; thread& operator=(const thread& other) = delete; thread& operator=(thread&& other) noexcept; //一个好的类一定实现一个异常安全的swap(effective c++) void swap(thread& other) noexcept; //**三、join、detach void join(); void detach(); bool joinable() const noexcept; //**四、class id id get_id() const noexcept; native_handle_type native_handle(); //**五、 static unsigned hardware_concurrency() noexcept; }
一、构造:
暂时忽略默认构造函数,只剩下一个构造函数了,这是一个可变模板参数的构造函数,并且使用了完美转发。下面针对参数讨论三点
1、Callable函数对象(callable object,标准库这样叫的),有四种:function、member function、function object、lambda,具体使用如下代码所示:
#include<thread> #include<iostream> class BackgroundClass{ public: void operator()()const{ std::cout << "function object" << std::endl; } void ClassMem()const{ std::cout << "member function" << std::endl; } }; void Func(){ std::cout << "function"<<std::endl; } int main(){ //1 //std::thread myThread(BackgroundClass()); 这是构建一个函数myThread,返回thread,参数是一个临时对象 //采用加括号std::thread myThread((BackgroundClass())) //或利用统一初始化std::thread myThread{BackgroundClass()}; std::thread myThread((BackgroundClass())); //2 //std::thread myThread(&BackgroundClass::ClassMem,BackgroundClass()); //3 //std::thread myThread([](){std::cout << "lambda" << std::endl; }); //4 //std::thread myThread(Func); myThread.join(); return 0; }
2、给callable object 传递参数:
正如可变参数模板使用一样,只要将需要的参数按顺序写到callable object后面即可。
需要注意的是,thread会将传递给他的参数原照原拷贝下来(实参是指针就拷贝指针;实参是对象就拷贝对象,即使形参是引用),然后thread在函数调用时候,利用这些拷贝下来的参数去调用。
int main(){ std::string str(“jia”); std::thread myThread([](std::string&str){ str += ”feng”; }, str); myThread.join(); std::cout << str << std::endl; //print:jia return 0; }
lambda形参为引用就是为了改变str,但是thread会把str拷贝到临时对象,然后用这个临时对象去调用这个lambda,lambda引用的是这个临时对象,当线程结束时候,临时对象销毁,并没有达到修改str的目的。要修改必须要让thread知道你传递是的”引用”,这可以利用std::ref(str)做到。
td::thread myThread([](std::string&str){ str += ”feng”; }, std::ref(str)); //print::jiafeng
如果使用指针是不会出现这个问题的,因为同一个进程同一地址空间。
std::thread myThread([](string* pstr){ (*pstr) += ”feng”; }, &str); //print:jiafeng
注意智能指针unique_ptr,他们有拷贝构造只有移动,因此只能用移动构造传参:
void ProcessBigObject(std::unique_ptr<big_object>); std::unique_ptr<big_object> p(new big_object); std::thread t(ProcessBigObject,std::move(p));
这里首先将big_object move到internal storage然后再move到函数调用。
3、thread对象创建效果:使用默认构造函数只是创建一个标记,并未有任何实质线程,get_id()返回默认结果,joinable()返回false;利用函数对象创建线程对象,线程会立刻加载执行,如果未能成功加载就会抛出异常。
二、拷贝、赋值:
thread跟unique_ptr、IO一样,没有拷贝构造、赋值,只有移动构造,移动赋值,这注定了thread跟unique_ptr相似,里面的真正线程只能在thread对象之间转移,不能出现两个thread对象内部是同一个线程,就像两个unique_ptr不能指向同一个对象一样。
1、拥有权转移:
void some_func(); void som_other_func(); std::thread t1(some_func); //t1拥有some_func std::thread t2(some_other_func); //t2拥有some_other_func t1=std::thread(some_other_func); //程序崩溃解释如下
临时对象激活t1的移动构造函数,在得到some_other_func之前,t1会先调用自己的析构函数,由于t1没有调用detach或join(后面详细介绍),因此析构函数会调用std::terminate程序崩溃
std::thread t3; //t3什么都没有 t3=std::thread(some_func); //激活t3的移动构造,t3此时拥有some_func std::thread t4; t4=std::move(t3); //将t3的some_func转交给t4
2、thread对象作为函数调用参数、返回对象:
void func1(std::thread); func1(std::move(t1)); func1(std::thread(some_func)); std::thread g(){ //1 return std::thread(some_func); std::thread t(some_func); return t; }
三、join、detach:
当一个thread对象构建完后、生命有效期内必须调用join或detach,如果不这样做,thread对象的生命期结束,调用析构函数时会调用std::terminate(),致使应用程序over。
当你不需要等待一个线程的执行结果时,在构建完thread对象后直接调用detach即可,往后拖延一方面有异常危险,一方面是资源的浪费。
当你需要等待一个线程的执行结果时,必须使用join。使用join的麻烦在于“将join放在什么位置?”,如果太靠前,主线程阻塞等待辅助线程结束,起不到并行的目的,如果放的太靠后,从thread对象构建到join的调用中间代码很多,很多代码意味着出现异常的可能性很大。join是必须调用的,即使中间代码抛出异常,否则即使你处理了中间代码抛出的异常,应用程序照样over,因为join不调用辅助线程会over整个应用程序。
启发于RAII,使用对象管理资源(effective c++),创建一个对象管理thread的join:
class ScopedThread{ std::thread m_thread; public: //线程只能移动,传参要用std::move()或临时对象 explicit ScopedThread(std::thread t) :m_thread(std::move(t)){ if (!m_thread.joinable()){ //传递进来的线程是空的 throw std::logic_error(“no thread have constructed!”) } } ~ScopedThread(){ m_thread.join(); } //禁止拷贝、赋值 ScopedThread(const ScopedThread&) = delete; ScopedThread& operator=(const ScopedThread&) = delete; } //使用: int main(){ //不管后面是否抛出异常,都能保证thread::join调用 ScopedThread(std::thread(some_func)); some_func_with_exception(); return 0; }
四、class id:
线程的唯一标识类,定义了各种比较操作符,因此可以用作关联容器中的索引。所有空线程id一样,如果两个id相等要么指向同一个线程,要么指向的两个线程实际都是空的。std::cout<<id<<std::endl;输入结果依赖于平台,因为标准只规定了两个不一样的线程输出结果不一样就行了,没有太多。
五、hardware_concurrency:
如果调用成功就返回当前应用程序可以支持的线程数,否则返回0。这个函数不是一定调用成功的,因此调用后要进行判断。