文章目录
内容概要:
- 启动线程,并通过几种方式为新线程指定运行代码
- 等待线程完成和分离线程并运行
- 唯一识别线程
1 线程的基本管控
每个c++程序都含有至少一个线程,即运行main()
的线程,它由c++运行时(C++ runtime)系统启动。随后,程序就可以发起更多的线程,它们以别的函数作为入口(entry point)。这些新线程连同起始线程并发运行。
当main()
返回时,程序就会退出;同样,当入口函数返回时,对应的线程随之终结。我们会看到,如果借std::thread
对象管控线程,即可选择等它自然结束。
1.1 发起线程
线程通过构建std::thread
对象而启动,该对象指明线程要运行的任务。
#include <thread>
void do_some_work();
std::thread my_thread(do_some_work);
与c++标准库中的许多类型相同,任何可调用类型(callable type)都适用于std::thread
。于是,可以设计一个带有函数调用操作符(function call operator)的类,并将该类的实例传递给std::thread
的构造函数:
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
构造std::thread
实例时,提供了函数对象f
作为参数,它被复制到属于新线程的存储空间中,并在那里被调用,由新线程执行。故此,副本的行为必须与原本的函数对象等效,否则运行结果可能有违预期。
将函数对象传递给std::thread
的构造函数时,要注意防范所谓的"C++最麻烦的解释"(C++‘s most vexing parse)。如果,传入的是临时变量,而不是具名变量,那么调用构造函数的语法有可能与函数声明相同。遇到这种情况,编译器就会将其解释成函数声明,而不是定义对象。
例如:std::thread my_thread(background_task());
为临时函数对象命名即可解决问题,做法是多用一对圆括号,或采用新式的统一初始化语法(uniform initialization syntax,又名列表初始化),例如:
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
为了避免这种问题,还能采用lambda表达式。
std::thread my_thread([]{
do_something();
do_something_else();
});
一旦启动了线程,就需要明确是等待它结束,还是由它独自运行。假如std::thread
对象销毁之际还没有决定好,那std::thread
的析构函数将调用std::terminate()
终止整个程序。
如果选择了分离,且分离时新线程还未结束运行,那它将继续运行,甚至在std::thread
对象销毁很久之后依然运行,它只有最终从线程函数返回时才会结束运行。那么在线程运行结束前,我们需保证它所访问的外部数据始终正确,有效。例如:
struct func
{
int& i;
func(int& i_):i(i_){}
void operator()()
{
for(unsigned j=0;j<10000000;++j)
{
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();
}
因此,以下做法极不可取:意图在函数中创建线程,并让线程访问函数的局部变量。除非线程肯定会在该函数退出前结束。
处理上述问题的另一种方法是,汇合新线程,此举可以确保在主线程的函数退出前,新线程执行完毕。
1.2 等待线程完成
若需等待线程完成,可调用std::thread
实例的成员函数join()
实现。
只要调用了join()
,隶属与该线程的任何存储空间即会因此消除,std::thread
对象遂不再关联到已结束的线程。事实上,它与任何线程均无关联。其中的意义是,对于某个给定的线程,join()
仅能调用一次;只能std::thread
对象增经调用过join()
,线程就不再可汇合(joinable
),成员函数joinable()
将返回false
。
1.3 在出现异常的情况下等待
join()
的调用需要小心地选择执行代码的位置,原因是,如果线程启动以后有异常抛出,而join()尚未执行,则该join()
调用会被略过。
一般地,若调用join()
仅仅是为了处理没有出现异常的情况,那么万一发生异常,我们也仍需调用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();
}
try/catch
块的使用则保证了新线程在函数f()
退出前终结(无论f()
是正常退出还是因为异常而退出)。try/catch
块稍显冗余,还容易引发作用域轻微错乱。假如代码必须确保新线程先行结束,之后当前线程的函数才退出,那么关键在于,全部可能得退出路径都必须保证这种先后次序,无论是正常退出,还是抛出异常。
为实现此目标,可以运用标准的RAII
手法,在其析构函数中调用join()
,代码如下:
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()
末尾时,按构建的逆序,全体局部对象都会被销毁。因此类型thread_guard
的对象g首先被销毁,在其析构函数中,新线程汇合。即便do_something_in_current_thread()
抛出异常,函数f()
退出,以上行为仍然会发生。
thread_guard
的析构函数先调用join