介绍线程的基本管控,包括线程的发起,等待,异常条件下如何等待, 后台运行,线程的join,detach,以及识别线程,归属权转移,如何管理等等。
线程发起
可以传入用户函数,仿函数,lamda表达式
仿函数作为参数
1 2 3 4 5 6 7 8 9 10 11
| class MyFunc{ public: void operator()(std::string str){std::cout<<} }; MyFunc myfunc; ###############错误初始化:直接传入 std::thread t2(myfunc()); t2.join(); #################正确初始化 std::thread t3((myfunc()); std::thread t4{mgfunc()};
|
为什么不能直接传入,还要加一层括号或者大括号?
因为编译器会把t2当作一个函数对象,返回值是thread,参数是一个返回值为back_task,参数为空的函数指针
1
| "std::thread (*)(background_task (*)())"
|
lambda作为参数
1 2 3 4
| std::thread t4([](std::string str) { std::cout << "str is " << str << std::endl; }, hellostr); t4.join();
|
线程等待
在局部域或者main函数中,如果启动了一个子线程,可能子线程还没运行就被回收了,如main函数先一步结束释放资源,调用线程的析构函数, 执行terminate操作
可以通过join的方式,让某子线程加入所属线程
1 2 3 4 5
| std::string hellostr = "hello world!";
std::thread t1(thead_work1, hellostr);
t1.join();
|
线程分离
线程允许在后台独自运行,可能会出现可预期的错误
- _i 绑定到 some_local_state 的引用(内存地址)
- 线程内部通过 _i 访问 some_local_state 的内存
- some_local_state 在 oops() 退出时被销毁,_i 成为悬空引用
- 访问已释放的内存 → 未定义行为(崩溃或数据错误)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct func { int& _i; func(int & i): _i(i){} void operator()() { for (int i = 0; i < 3; i++) { _i = i; std::cout << "_i is " << _i << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } } }; void oops() { int some_local_state = 0; func myfunc(some_local_state); std::thread functhread(myfunc); functhread.detach(); }
oops();
std::this_thread::sleep_for(std::chrono::seconds(1));
|
如何解决?
- 通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
- 将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
- 将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。
如何保证线程的安全性
异常处理
对主线程可能出现崩溃的地方进行try-catch,保证不会因为主线程崩溃导致子线程的重要任务没有被执行
在catch的部分就可以进行join
1 2 3 4 5 6 7 8 9 10 11 12 13
| void catch_exception() { int some_local_state = 0; func myfunc(some_local_state); std::thread functhread{ myfunc }; try { std::this_thread::sleep_for(std::chrono::seconds(1)); }catch (std::exception& e) { functhread.join(); throw; } functhread.join(); }
|
或者采用RAII的思想,借鉴go语言的defer类,原理是写一个包装类,构造传入的参数作为析构要执行的函数.这样在主线程执行完毕进行资源回收的时候,自动调用defer的析构函数,就可以帮我们自动完成资源回收等任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class thread_guard { private: 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; }; #########################
void auto_guard() { int some_local_state = 0; func my_func(some_local_state); std::thread t(my_func); thread_guard g(t); std::cout << "auto guard finished " << std::endl; } auto_guard();
|
慎用隐式转换
C++中会有一些隐式转换,比如char* 转换为string等。这些隐式转换在线程的调用上可能会造成崩溃问题
下面是buffer作参数传递时退化为 char*(数组→指针的隐式转换)
1 2 3 4 5 6 7
| void danger_oops(int som_param) { char buffer[1024]; sprintf(buffer, "%i", som_param); std::thread t(print_str, 3, buffer); t.detach(); std::cout << "danger oops finished " << std::endl; }
|
这里实际上传入的是字符数组的首地址指针,指针类型经过打包不变,经过move也不变(char* &&=char*),所以还是原指针,但原栈空间可能因为作用域的结束而被释放,故而成为悬空指针,尝试通过悬空指针访问已释放的内存
引用参数
如果线程要的回调函数参数必须要是引用类型的话,需要利用std::ref将参数显示转化为引用对象传递给线程的构造函数.如果不加会在参数打包过程中被转化为值类型,并最终转化为右值,导致在最后传参的时候与函数定义参数不符.
至于为什么加上std::ref就可以保证引用效果可以看上一篇thread源码讲解
绑定类的成员函数
有时候我们需要绑定一个类的成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| class X { public: void do_lengthy_work() { std::cout << "do_lengthy_work " << std::endl; } };
void bind_class_oops() { X my_x; std::thread t(&X::do_lengthy_work, &my_x); t.join(); }
|
这里注意一下,如果thread绑定的回调函数是普通函数,可以在函数前加**&**
或者不加**&**
,因为编译器默认将普通函数名作为函数地址