线程管理
Lunching Thread
C++ 11中对线程的管理集中在std::thread这个类中,创建线程的方式包括:
- 使用回调函数
- 使用函数对象
- 使用Lambda表达式
void funcptr(std::string name){
cout << name<<endl;
}
struct Functor{
void operator()(string name){
cout<<name<<endl;
}
};
void runCode(){
//使用function pointer
std::thread t1(func1,"abc");
t1.detach();
//使用Functor
Functor f;
std::thread t2(f,"Functor thread is running");
t2.detach();
//使用lambda
std::string p = "lambda thread is running";
std::thread t3([p]{
cout<<p<<endl;
});
t3.join();
}
std::thread是C++11引入的用来管理多线程的新类,是对UNIX C中pthread_t结构体的封装,构造时调用pthread_create传入pthread_t和回调函数指针
typedef pthread_t __libcpp_thread_t;
class _LIBCPP_TYPE_VIS thread{
__libcpp_thread_t __t_; //pthread_t
...
}
thread::thread(_Fp&& __f, _Args&&... __args){
...
int __ec = __libcpp_thread_create(&__t_, &__thread_proxy<_Gp>, __p.get());
if (__ec == 0)
__p.release();
else
__throw_system_error(__ec, "thread constructor failed");
}
int __libcpp_thread_create(__libcpp_thread_t *__t, void *(*__func)(void *),
void *__arg){
return pthread_create(__t, 0, __func, __arg);
}
std::thread的构造函数中前两个参数均为右值引用,第二个参数将传入的lambda表达式或者functor通过__thread_proxy<_Gp>转化成C函数指针(void *(*__func)(void *),这个问题可参考之前对C++11中 move语义的介绍。std::thread对象在创建后,如果不做其它操作,线程立刻执行,这里称这个线程为worker_thread,称发起worker_thread的线程为launch_thread。
如果std::thread对像在被销毁前未执行join()或detach()操作,则在其析构函数中会调用std::terminate造成系统崩溃。因此需要确保所有创建的std::thread对象都能被正常释放,在《C++ Concurrency in Action》中,提到了一种方法:
class thread_guard{
std::thread &t;
public:
explicit thread_guard(std::thread& t_):t(t_){}
~thread_guard(){
if(t.joinable()){
t.join();
}
}
thread_guard(thread_guard const& ) = delete;
thread_guard& operator=(thread_guard const& ) = delete;
};
void runCode(){
...
std::thread t(f);
thread_guard g(t); //g在t之前释放,保证join的调用
do_something_in_current_thread();
}
由于thread_guard对象总是在std::thread对象之前析构,因此可以在t析构之前调用join函数,保证t可以安全释放。
这种方式是所谓的RAII(Resource Acquisition Is Initialization),即通过构造某个对象来获得某个资源的控制,在该对象析构时,释放被控制的资源。也就是将某资源和某对象的生命周期做绑定,在C++中这是一种很常用的设计方式,背后的原因是C++允许栈对象的创建和析构,后面在讨论mutex时还会继续用到这种技术
Join & Detach
join()是launch_thread和worker_thread的一个线程同步点,launch_thread会在调用join()后等待worker_thread执行完成后继续执行
std::string p = "lambda";
//using lambda expression as a callback function
std::thread td([p]{cout<<p<<" thread is running"<<endl;});
cout<<"lanched thread is running"<<endl;
td.join();
cout<<"lanched thread is running"<<endl;
td.joinable(); //return false
- 如果
td在main thread执行td.join()之前完成,则td.join()直接返回,否则launch_thread会暂停,等待td执行完成 - 如果不调用
td.join()或td.detach(),在td对象销毁时,在std::thread的析构函数中,如果则系统会发出std::terminate的错误 td在调用join后,joinable转态变为false,此时td可被安全释放- 确保
join()只被调用一次
如果使用td.detach()则workder_thread在创建后立刻和launch_thread分离,launch_thread不会等待workder thread执行完成。即两条线程没有同步点,各自独立执行
void runCode()
{
cout << "lanched thread is running" << endl;
std::string p = "lambda";
std::thread td([p] {
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
cout << p << " thread is running" << endl;
});
td.detach();
cout << "lanched thread is ending" << endl;
}
上述代码中令workder thread暂停5s,则launch_thread继续执行,不会等待worker_thread执行完,如果将detach()改为join()则launch_thread会阻塞等待
//detach
lanched thread is running
lanched thread is ending
//join
lanched thread is running
lambda thread is running
lanched thread is ending
向线程传递参数
向std::thread构造函数传递参数的规则为:
- 第一个参数为函数指针,可以是functor或者lambda表达式,在第一节中已经介绍
- 后面参数为该函数指针需要用到的参数
观察前面的std::thread的构造函数可知,传递的参数均是拷贝到线程自己stack中,但是有某些场景,需要修改lauch_thread所在线程的局部变量,这是需要将该变量的引用传递给worker_thread。例如下面的例子中需要在worker_thread中修改data变量
void updateData(widget_data& data);
void oops(){
widget_data data;
std::thread t(updateData, data); //这里传过去的是data的copy
t.join();
process_widget_data(data);
}
参考之前文章中对bind函数的介绍,可知这里只需要一个很小的改动,使用std::ref(x),即可把data从传拷贝变成传引用:
std::thread t(updateData, std::ref(data))
如果传引用或者指针要特别注意变量的生命周期,如果该变量的内存在线程还未结束时被释放则会引
undefined behavior
为了进一步加深对std::thread构造函数的理解,继续参考bind函数不难发现,std::thread的构造函数和bind的传参机制是相同的,这意味者只要第一个函数时一个函数指针,后面是该函数的参数即可,因此可以不局限于使用第一小节介绍的三种构建线程的方式,比如:
class X{
public:
void do_some_work(){
cout<<"do_some_work"<<endl;
};
};
X x;
std::thread t(&X::do_some_work, &x);
t.detach();
上述代码中,X::do_some_work方法的第一个参数为this指针,因此可将x取地址后传入,可达到相同效果。