0
点赞
收藏
分享

微信扫一扫

《线程管理:线程基本操作》


目录

  • ​​线程管理​​
  • ​​启动线程与(不)等待线程完成​​
  • ​​特殊情况下的等待(使用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()) //1
t.join(); //2
}
thread_guard(thread_guard const&) = delete; //3
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();
} //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); //1
t.detach(); //2
}
else
{
process_user_input(cmd);
}
}

}

用户选择打开一个新文档,需要启动一个新线程去打开新文档(如step1),并分离线程(如step2)。与当前线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit_document函数可以复用,并通过传参的形式打开新的文件。


举报

相关推荐

0 条评论