使用要点:
- 如果thread对应的例程还没结束,或者thread对象当前处于joinable(待收尸)状态,此时销毁thread对象都会导致程序崩溃(确切的说是会直接终止程序)。
- thread::join 和 thread::detach 返回后,会让 thread 进入 unjoinable(不可收尸) 状态,unjoinable(不可收尸) 状态下的 thread 可以安全销毁。
- 使用thread默认构造函数的实例是unjoinable(不可收尸) 的。
- 使用move以后thread会变成unjoinable(不可收尸) 。
- detach之后,线程和 thread 对象将不再有任何关系,此时我们可以随意销毁thread,当然,我们也永远无法 “触碰到” 线程,因为他已经由系统接管。
- thread 是 movable 的,但不是 copyable 的。对已经包含一个线程的thread再次使用 std::move 进行赋值会导致程序退出,即不要重复给thread move赋值。
- 对 unjoinable(不可收尸) 的 thread 对象使用 join() 会抛异常,因此再使用 join() 之前要先通过 joinable() 判断下。
小结:要么等待例程结束,要么在即将销毁thread实例之前判断joinable状态并调用join或detach,要么这个thread实例是默认构造出来的,要么已经调用move把线程转移给其他thread实例了,否则任何形式的thread实例销毁都会导致程序崩溃。另外,join会阻塞,detach不会阻塞。
用法一:使用可调用对象
#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
template <typename T>
class ThreadRoutine
{
public:
ThreadRoutine() = delete; //T 类型位置不考虑T进行默认构造,简单起见直接禁用默认构造
//ThreadRoutine(const ThreadRoutine&) = delete; //禁用拷贝构造 (放开编译会报错,为什么???)
ThreadRoutine& operator=(const ThreadRoutine& ins) = delete; //禁用拷贝赋值
ThreadRoutine(T input) {
_Input = input; //T 必须支持拷贝构造和拷贝赋值
}
void operator()()
{
cout << "_Input's size is -> " << sizeof(T) << endl;
}
T _Input;
//T & _R_Input; //如果使用引用则需要考虑内存合法性问题,因为传入的参数可能在线程还没有执行时就已经被释放,所以建议使用拷贝值的方式传参,或者使用 堆内容,然后使用智能指针进行控制
};
int main()
{
ThreadRoutine<int> t(10);
thread routine(t);
routine.join();
std::cout << "Hello World!\n";
while (1) Sleep(1000);
}
用法二:使用函数
#include <iostream>
#include <thread>
#include <memory>
#include <Windows.h>
using namespace std;
struct RoutineInput {
char c;
int i;
long l;
void * data;
};
void Routine(shared_ptr<RoutineInput> input) {
cout << "sizeof intput -> " << sizeof(*input) << endl;
}
int main()
{
std::shared_ptr<RoutineInput> input = make_shared<RoutineInput>(); //使用智能指针防止routine中访问已被释放的内存
thread routine(Routine, input);
routine.join();
std::cout << "Hello World!\n";
while (1) Sleep(1000);
}
需要注意:
1)使用这种方法时,传递给函数的参数是传值的(即便函数入参明确表示是引用类型也不生效,这很重要),如果想传字符串而入参是字符数组,那么请务必把字符数组显式转换为std::string,因为std::string底层实现是计数模式,所以在发现引用即将为0时会进行一次 copy-on-write 创建一个新的副本。但是作为C/C++开发者,心里要有数,尽量避免危险的指针使用场景。
2)上面提到了只会传值,那么如果确实想传引用怎么办? 答案是使用 std::ref 进行强制转换。
小技巧:
前面提到了thread必须在unjoinable的状态下才可以被销毁,否则会导致程序终止。那么我们可以使用 RAII 策略加保险。
Demo:
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_) :
t(t_)
{}
~thread_guard()
{
if (t.joinable())
{
t.join();
//t.detach(); //如果不希望阻塞就使用detach,否则使用join等待线程结束
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
};
struct func;
void f()
{
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
这里利用的是 “函数调用栈释放是从栈顶向栈底的方向” 这一策略, thread_guard 局部变量在 thread 局部变量之后定义,那么在释放栈的时候 thread_guard 的析构函数会 早于 thread 实例销毁。因此可以在析构函数中转变 thread 为 unjoinable。