[C++ 并发编程] 线程管控
目录
1. 线程的基本管控
每个C++程序至少有一个主线程,即运行main()的线程。
1.1 发起线程
class task
{
public:
void operator()() const
{
do_something();
do_other_something();
}
};
task f;
std::thread my_thread(f);
1.1.1 线程的声明
任何可调用对象(如函数指针、函数对象、lambda等,即能进行函数调用操作的对象)都适用于std::thread。 所以。在这里我们写了一个task类,重载运算符"()",将其写为能进行函数调用操作的类,生成其对象,初始化线程my_thread。f会被复制到属于新线程的存储空间中,并在那里被调用,由新线程执行。这里要注意一个问题,只要这个C++语句有可能被解释为函数声明,那么编译器就会把它解释为函数声明。如:
std::thread my_thread(func());
[函数返回类型] [函数名][调用参数]
这种被解释为函数申明的就会报错,解决办法:
std::thread my_thread((func())); //使用func()函数返回的临时对象
std::thread my_thread{func()}; //列表初始化
1.1.2 汇合或分离线程
在线程对象销毁前,必须决定好,是汇合线程(join)还是分离线程(detach),否则在线程对象销毁时,将会在析构时,调用std::terminate()函数终止整个程序。
1.1.3 线程的启动
线程一旦初始化,就开始启动。 我们只需要在线程对象销毁前,决定好,就行了,不用管线程是在join或detach之前结束,还是在detach之后结束。
1.1.4 线程的运行
在线程运行中,注意未定义行为的发生。如:
struct func
{
int& i;
func(int *i_):i(i_){}
void operator()()
{
for(int i=0;i<1000000;++i)
do_something(i);
}
}
void oops()
{
int k=0;
func my_func(k);
std::thread my_thread(my_func);
my_thread.detach();
}
func类里,i用的是引用类型,当初始化my_func时,i则初始化为临时变量k的引用,当my_thread线程开始后,结束前,oops函数就已经结束了,临时变量k被销毁,则线程里继续访问k的引用时,将发生未定义行为。避免此问题的发生:1.线程完全自含,所有调用的变量都由线程本身生成,2.汇合线程(join),等待此线程结束再继续执行。
1.2 等待线程完成(join)
汇合线程(join):
join()简单粗暴,会一直等待线程结束。当join()执行完成,线程结束后,会把线程的所有资源释放掉,std::thread关联的对象,将不再有效。所以,join()有且只能调用一次,可使用joinable()进行判断,没有调用过join为true,否则则为false。
if(my_thread.joinable())
{
my_thread.join();
}
1.3 出现异常情况下等待线程完成
void f()
{
int k=0;
func my_func(k);
std::thread t(my_func);
do_something();
t.join();
}
若do_something函数发生了异常,f()函数会终止,接着系统会销毁栈对象,首先销毁线程对象t,因为此时没有join或detach,所以整个程序结束运行。所以当异常发生时,也必须要保证,线程已经join或detach。
void f()
{
int k=0;
func my_func(k);
std::thread t(my_func);
try
{
do_something();
}
catch(...)
{
t.join();
throw;
}
t.join();
}
关键:全部可能的退出路径都必须保证,线程对象,先join或detach再销毁,这种先后次序。
所以我们可以设计一个类来管理线程,避免发生terminate()函数:
class thread_guard
{
public:
explicit thread_guard(std::thread& t_):t(t_){}
~thread_guard()
{
if(t.joinable())
t.join();
}
thread_guard(thread_guard const &)=delete;//(1)
thread_guard& operator=(thread_guard const&)=delete;//(2)
private:
std::thread& t;
};
(1)、(2):避免拷贝构造和赋值操作,因为其可能会导致两个thread_guard对象管理同一个线程,一是新产生的对象生存周期可能很长,比与之关联的线程还要长,线程结束,销毁后,则此类再销毁,就会调用joinable,而此线程已被销毁,所以会发生未定义行为。二是两个对象有可能同时销毁,joinable判断为true,join被执行了两次。
1.4 在后台运行线程
使用detach把线程分离,将无法等待它完结,也不能获得和它关联的std::thread对象,其归属权和控制权都将交给C++运行时库,由此保证,一旦线程退出,那么其资源都将被正确回收。
detach和join一样,只有当joinable()返回true时,我们才能调用detach。
2. 向线程传递参数
若要向线程提供参数,只需在线程构造函数后添加更多参数即可。
2.1 参数传递过程
请牢记,线程具有自己的内部存储空间,参数会按照默认的方式先复制到该处,新创建的执行线程才可以直接访问它。然后,赋值到该处的参数会以右值形式传给指定的函数或可调用对象。即便是引用,上述过程也会发生。
void func(int i,std::string const&s);
std::thread t(func,3,"hello");
"hello"是const char*类型,会先以此类型,赋值到线程t的空间里,然后其才会作为右值的形式赋值给s,即std::string const &s="hello"。 所以,在这里作为线程绑定的函数的参数,必须为const类型,因为只有常量引用类型,才能赋值字面值字符串常量。
2.2 使用std::ref传递引用参数
如果我硬是要用非常量引用的参数呢?
void func(int i,std::string &s);
std::string str="hello";
std::thread t(f,3,std::ref(str));
那么就要先把hello从字面值常量转换为string类型,再执行std::ref函数,此函数会返回一个对象,包含给定的引用,并且此对象是可以拷贝的,这样就能使用非常量引用的参数了,其与std::bind的使用是一样的。
2.3 使用std::move传递参数
有些对象,它的值只能移动不能复制。如:unique_ptr,它指向的对象只能有唯一的unique_ptr指向它,其对象间的转移,会令上一个unique_ptr为NULL。这种动态对象,它作为线程绑定的函数的参数,要传递对象的话,就要使用std::move(将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。),如:
void func(std::unique_ptr<class> a);
std::unique_ptr<class> p(new class);
std::thread t(func,std::move(p));
注:std::move(p)未进行移动操作,只是把p转换为右值,然后创建线程时,再发生拷贝,再进行右值赋值。
2.4 第二个隐藏的参数
std::thread和std::bind一样,当绑定为某个类的成员函数时,要传递该类的对象的指针。如:
class A
{
void a();
...
};
A my_A;
std::thread t(&A::a,&my_A);
2.5 参数传递注意
参数的传递要注意避免悬空指针,如:
void func(int i,std::string const&s){
...
};
void _f()
{
const char*data="hello";
std::thread t(func,3,data);
t.detach();
}
在func执行时,_f函数可能已经退出了,而此时,data可能刚复制到新线程的空间,std::string const&s=data;还没执行,而data已经被销毁,从而发生未定义行为。
3.移交线程的归属权
3.1 std::thread的唯一性
std::thread和std::unique_ptr的性质具有一定的相似性,std::thread和std::unique_ptr一样,std::thread也只能管控一个执行线程,std::thread的实例间只能移动,不能复制。任何时候都只有唯一的std::thread对象与执行函数关联,其归属权在线程间只能转移,不能复制。
3.2 使用std::move移交线程归属权
std::move(将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。),转移后,原对象将失去绑定,并允许绑定另一个执行函数。
临时变量的转移,不需要使用std::move,因为其移动操作会隐式进行。如
std::thread t1=std::thread(func);
3.3 小心线程的遗弃
在std::thread对象之间转移管控的线程时,要小心线程被遗弃,因为:在转移之前,线程一定是还没有detach和join的,因为这两种行为,前者会导致线程移至后台运行,std::thread对象与其失联,后者会导致线程直接运行结束,所以如果线程被遗弃了,那么就会执行std::terminate()函数,程序终止。