目录
- 线程管理
- 启动线程与(不)等待线程完成
- 特殊情况下的等待(使用trycath或rall)
- 后台运行线程
线程管理
启动线程与(不)等待线程完成
提供的函数对象被复制到新的线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。
class background_task
{
public:void operator()() const{do_something();do_something_else();}
};background_task f;std::thread my_thread(f);
注意,如果传递的是一个临时变量,而不是一个命名变量,cpp编译器会将其解析为函数声明,而不是类型对象的定义。
当我们不等一个线程返回时,我们需要先将数据复制到线程中,这样就不会产生访问到已经销毁的变量的问题了。
就如下所示,函数已经返回,线程依旧能够访问到局部变量
struct func
{int& i;func(int& i_) : i(i_) {}void operator() (){for (unsigned j=0 ; j<1000000 ; ++j){// 1 潜在访问隐患:空引用do_something(i);}}
};
void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach();
}
// 2 不等待线程结束
// 3 新线程可能还在运行
oops函数执行完成时,线程中的函数还在执行,还会访问已经销毁了的some_local_state变量,因为它持有的是该变量的指针。所以需要将数据复制到线程中,原始对象被销毁也不妨碍线程中变凉了。当然,使用访问局部变量的函数去创建线程不是很好。
此外,可以通过join()函数来确保线程在主函数完成前结束,可以确保局部变量在线程完成后才销毁。
注意,只能对一个线程使用一次join,一旦使用过join,thread对象就不能再次汇入。
特殊情况下的等待(使用trycath或rall)
对于一个未销毁的thread对象,如果想分离线程,在线程启动后直接使用detach()进行分离。如果想等待线程,就需要思考好join位置,也要考虑抛出异常给join带来的生命周期问题。
如下:使用了try/catch块确保线程退出后函数才结束
struct func; //定义代码上面有
void f()
{int some_local_state = 0;func my_func(some_local_state);std::thread t(my_func);try{do_something_in_current_thread();}catch(...){t.join();throw;}t.join();
}
接下来介绍使用RALL等待线程完成
class thread_guard
{std::thread& t;
public:explicit thread_guard(std::thread& t_): t(t_) {}~thread_guard(){if(t.joinable()) //1t.join(); //2}thread_guard(thread_guard const&) = delete; //3thread_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();
} //4
当线程执行到4,局部对象就要被逆序销毁了。所以,对象g第一个被销毁,线程在析构函数中,判断是可join的,随之执行join。所以即使do_something_in_current_thread函数跑出异常,这个销毁依旧会发生。
还有个需要注意的地方,拷贝构造函数和拷贝赋值操作做标记为 =delete,编译器不会自动生成。因为直接对对象进行拷贝或者赋值可能会丢失已经join的线程。
如果不想等待线程结束,可以分离线程,从而避免异常。不过这就打破了线程与std::thread对象联系。即使线程仍然在后台运行,detach操作也能确保std::terminate()在std::thread对象销毁时才调用。
后台运行线程
一个线程在后台运行,就不能与主线程直接交互,分离的线程也不能join,不过c++保证,当线程退出时,相关资源能够正确回收。
分离线程又称守护线程。在UNIX中,守护线程指的是没有任何显式接口,并在后台运行的线程。特点是长时间运行。
下面介绍一下分离线程的使用场景:
让一个文字处理应用同时编辑多个文档。每个文档窗口看起来完全独立,每个窗口也都有自己独立的菜单选项,但他们却运行在同一个应用实例中。一种内部处理方式是,让每个文档处理窗口拥有自己的线程。每个线程运行同样代码,并隔离不同窗口处理的数据。所以没打开一个文档就要启动一个新线程,因为是对独立文档进行操作,所以没有必要等待其他线程完成,可以让文档处理窗口运行在分离线程上。
void edit_document(std::string const& filename)
{open_document_and_display_gui(filename);while(!done_editing()){user_command cmd = get_user_input();if(cmd.type == open_new_document){std::string const new_name = get_filename_from_user();std::thread t(edit_document,new_name); //1t.detach(); //2}else{process_user_input(cmd);}}}
用户选择打开一个新文档,需要启动一个新线程去打开新文档(如step1),并分离线程(如step2)。与当前线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit_document函数可以复用,并通过传参的形式打开新的文件。