02 线程管理 | 《C++ Concurrency In Action》笔记
本系列笔记参考《C++ Concurrency in Action, 2nd Edition》及其中文翻译。
线程管理基础
特殊情况下的等待
异常捕捉
当在线程运行后产生的异常,会在 join() 调用之前抛出,这样就会跳过 join()。避免应用被抛出的异常所终止。通常,在无异常的情况下使用 join() 时,需要在异常处理过程中调用 join(),从而避免生命周期的问题。
1 | void f() { |
RAII
1 | class thread_guard { |
后台运行线程
使用 detach() 会让线程在后台运行,这就意味着与主线程不能直接交互。如果线程分离,就不可能有 std::thread 对象能引用它。分离线程的确在后台运行,所以分离的线程不能汇入。不过 C++ 运行库保证,当线程退出时,相关资源的能够正确回收。
分离线程通常称为守护线程(daemon threads)。UNIX 中守护线程,是指没有任何显式的接口,并在后台运行的线程,这种线程的特点就是长时间运行。线程的生命周期可能会从应用的起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另外,分离线程只能确定线程什么时候结束,发后即忘(fire and forget)的任务使用到就是分离线程。
注意,启动线程后在线程销毁前要对其调用 join() 或 detach(),否则 std::thread 的析构函数会调用std::terminate 来终止程序。
传递参数
直接传参
向可调用对象或函数传递参数很简单,只需要将这些参数作为 std::thread 构造函数的附加参数即可。需要注意的是,这些参数会拷贝至新线程的内存空间中,即使函数中的参数是引用的形式,拷贝操作也会执行。
1 | void f(int i, std::string const& s); |
在上面的代码中,buffer 是一个指针,指向局部变量,然后此局部变量通过 buffer 传递到新线程中。此时,函数 oops 可能会在 buffer 转换成 std::string 之前结束,从而导致未定义的行为。因为,无法保证隐式转换操作和 std::thread 构造函数的拷贝操作的顺序,有可能 std::thread 的构造函数拷贝的是转换前的变量(buffer 指针)。解决方案就是在传递到 std::thread 构造函数之前,就将字面值转化为 std::string:
1 | std::thread t(f, 3, std::string(buffer)); |
相反的情形(期望传递一个非常量引用,但复制了整个对象)倒是不会出现,因为会出现编译错误,比如:
1 | void update_data_for_widget(widget_id w, widget_data& data); |
虽然 update_data_for_widget 的第二个参数期待传入一个引用,但 std::thread 的构造函数并不知晓,构造函数无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用 update_data_for_widget。但因为函数期望的是一个非常量引用作为参数,所以会在编译时出错。问题的解决办法很简单:可以使用 std::ref 将参数转换成引用的形式:
1 | std::thread t(update_data_for_widget, w, std::ref(data)); |
这样 update_data_for_widget 就会收到 data 的引用,而非 data 的拷贝副本,这样代码就能顺利的通过编译了。
传递函数指针
除了直接传参,还可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:
1 | class X { |
这段代码中,新线程将会调用 my_x.do_lengthy_work()
,其中 my_x 的地址作为对象指针提供给函数。也可以为成员函数提供参数:std::thread 构造函数的第三个参数就是成员函数的第一个参数,以此类推:
1 | class X { |
移动性
std::thread 的构造函数参数参数仅支持移动,不能拷贝。即在赋值的时候就发生了了所有权转移。
就像对象的所有权可以在多个 std::unique_ptr 实例中转移一样,线程的所有权可以在多个 std::thread 实例中转移,这依赖于 std::thread 实例的可移动且不可复制性。不可复制性表示在某一时间点,一个 std::thread 实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。
转移所有权
不要通过赋新值给 std::thread对象的方式来”丢弃”一个线程。赋值时会终止源线程,源线程很可能还没有执行完,此时会 crash。
02 线程管理 | 《C++ Concurrency In Action》笔记