文章目录
线程管理基础
启动线程
每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程(以main()为入口函数的线程)同时运行。
使用C++线程库启动线程,可以归结为构造 std::thread 对象,其构造函数传入的参数是可调用对象。
void do_some_work();
std::thread my_thread(do_some_work);
C++'s most vexing parse
需要注意一个关于C++语法解析的问题:“C++’s most vexing parse”
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
这里我们构造了一个函数对象f,并传入thread的构造函数中。如果我们直接这么写
std::thread my_thread(background_task());
我们本意是希望使用background_task的构造函数返回一个background对象,并直接用这个临时对象构造一个thread对象。但是在C++中,上面的表达式会被解析为:声明了一个my_thread函数,返回值为thread对象,参数为一个函数指针,这个函数指针指向的函数没有参数,且其返回值background_task对象。
为了避免这种歧义发生,除了提前声明一个函数对象之外,还可以:
- 新增一对括号
std::thread my_thread((background_task()));
- 使用初始化语法
std::thread my_thread{background_task()};
join或detach
thread对象构造完成(线程开始执行)之后,对象析构之前,我们必须选择是等待它(join)或者让它在后台运行(detach),如果你在thread对象析构前没有这么做,那么线程将会终止,因为thread的析构函数中调用了std::terminate()
。
- detach使得即使thread对象析构,线程也能继续运行,但是注意,我们要确保线程结束之前它所访问的数据都是有效的。
- 调用thread对象的join方法,函数将等待该线程完成,然后继续执行后续语句。join将清理线程相关的存储空间
thread对象只能join或detach一次,调用过join或detach的对象再调用joinable将返回false
在发生异常的情况下join
在使用detach的时候,我们通常在构造完thread对象就立即调用detach了;而join的位置则会选在thread对象析构之前的某个位置,如果在join之前发生了异常,函数将终止,join不会被调用。为了避免这种情况发生,我们可以加上try-catch语句,并且通常来说,我们希望即使发生异常也调用join方法等待。
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();
}
这种写法过于冗长,我们的目标本质上是希望线程完成之后函数再退出,有一种简洁的方式可以达到这个目的:资源获取就是初始化(RAII,Resource Acquisition Is Initialization)
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;
};
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();
}
当f函数执行结束时,局部变量析构的顺序与构造顺序相反,即先析构g,再析构t,在thread_gurad的析构函数中调用了join函数,即使do_something_in_current_thread发生异常,join函数也会被调用(实际并不是??)。因为线程只能被join一次,因此要先判断是否joinable。
这里删除了默认的拷贝构造函数和拷贝赋值函数,这是因为拷贝后的对象的生命周期可能会超出thread对象的作用域(例如函数返回一个thread_guard对象)
detach
一旦thread对象调用了detach,线程与thread对象将不再有关联,我们也没有直接的方式与线程通信,也不再能join或detach该线程,此时线程的所有权属于C++运行时库,它保证在线程退出时相关资源被回收。分离的线程通常称为守护进程,它们通常在程序的整个生命周期运行,做一些监控、清理工作。同样的thread对象只能被detach一次