【C++并发实战】(二)线程管理
本篇主要讲述线程的管理,主要包括创建和使用线程
启动线程
线程出现是为了执行任务,线程创建时会给一个入口函数,当这个函数返回时,该线程就会退出,最常见的main()函数就是主线程的入口函数,在main()函数返回时主线程就结束了。 如何启动一个线程呢?就如上所述,需要给线程对象一个入口函数。
#include <iostream> #include <thread> using namespace std; void hello() { cout << "hello world"; } int main() { thread t(hello); t.join(); return 0; }
上述代码中实现了一个简单的线程
一旦线程被启动,就必须要显式地决定是要等待线程完成还是让线程自己运行,这个决定必须要在线程对象销毁前完成,否则,程序将会被终止(抛出异常),如果不等待线程完成,则需要保证线程访问的数据必须是有效的,直到该线程终止,在这种情况下,必须要小心的使用局部变量,局部变量的指针和引用。
ps:需要注意的是等待线程完成和分离线程必须要在线程对象销毁前完成,并不是在线程函数执行结束前完成。原因是线程对象被销毁后不能直接与线程通信,将无法等待或者分离。 例如:
#include <iostream> #include <thread> #include <stdlib.h> #include <windows.h> using namespace std; void hello() { cout << "hello world"; } int main() { thread t(hello); sleep(3000); t.join(); return 0; }
这个代码中线程执行的hello函数已经结束,依然可以join
等待线程完成
在上面的代码例子中,使用了线程对象的join函数,这个函数的作用就是等待线程完成。同时需要注意,一个线程不能被join两次。可以通过线程对象的joinable()函数判断当前函数是否可以被join。
std::thread t(do_thread_work); if(t.joinable()) t.join();
分离线程
除了可以等待线程完成,还可以分离线程,让线程自己运行,也就是所谓的在后台运行线程,当线程对象被销毁后,线程可能仍在运行,此时,没有直接的方法可以与其通信,不能再通过线程对象获取到该线程,也不能再被join,所以要注意资源的请求和释放。
参照守护进程的概念,被分离的线程通常被称作守护线程。
std::thread t(do_some_background_work); t.detach(); assert(!t.joinable());
为了分离线程,线程对象必须与一个线程相关联,不能在没有线程关联的线程对象上使用detach(),join()函数也是同理,因此也可以使用joinable()函数来判断当前函数是否可以detach。
在异常环境下的等待
如上所述,必须在线程对象被销毁之前调用join()或者detach(),如果需要分离线程,一般会在线程对象创建好后立刻调用,这样问题不大。但是如果打算等待线程完成,就需要考虑在哪个位置调用join。如果在线程开始之后join执行之前发生了异常,对join的调用可能就会被跳过。所以应该确保join函数被调用,以免程序被终止。
std::thread t(my_function); try { do_something(); } catch { t.join(); throw(); } t.join();
当然还有其他的做法,利用raii的思想,类似智能指针,对thread对象进行封装。
#include <iostream> #include <thread> class thread_guard { public: explicit thread_guard(std::thread& t) :t_(t) {} ~thread_guard() { if (t_.joinable()) { t_.join(); } } private: std::thread& t_; }; void hello() { std::cout << "hello world"; } int main() { std::thread t(hello); thread_guard g(t); return 0; }
传递参数给线程函数
线程的入口是个函数,作为使用者肯定是想传递一些函数参数的。操作也比较简单,将额外的参数传递给std::thread的够凹函数就可以了。但是,需要重视的一点是,参数默认上会被复制到内部存储空间,然后在那新创建的线程可以访问这些参数,即便函数中的相应参数期待着引用
这个意思就是说,传递参数给县城函数就像是给普通函数传递参数一样,默认传递的是形参,而且就算你加了引用,传递的也是形参。是std::thread的构造函数做的这个事情,他无视函数所期望的引用盲目的复制了所提供的值
解决方案也很简单,使用std::ref()包裹你想要以引用传递的参数。
void update_data_for_widget(widget_id w, widget_data& data); void fun(widget_id w) { widget_data data; std::thread t(update_data_for_widget, w, std::ref(data)); }
如果不这么做,传递的data在线程函数中的改变是不会起作用的。
转移线程的所有权
和std::unique_ptr类似,std::thread是可移动并且非可复制的,这意味着一般不会有两个线程对象指向同一个线程实例,如果出现此情况会发生异常。如果同时给一个线程对象关联两个线程实例,也会触发异常。
void some_function(); void some_other_function(); std::thread t1(some_function); std::thread t2 = std::move(t1); t1 = std::thread(some_other_function); t3 = std::move(t2); t1 = std::move(t3); //此项操作将会终止程序!
标识线程
线程标识符是std::thread::id类型的,获取方式有以下两种
- 可以通过与线程相关联的std::thread对象调用get_id()方法获取。如果该线程对象没有相关联的线程,则此方法会返回一个默认构造的std::thread::id对象用来表示“没有线程”
- 另外可以通过std::this_thread::get_id()获取当前现成的id。
id可以*地被用于复制和比较,或者是被排序。