线程基础
声明
此随笔内容是根据大佬博客进行学习,如想学习,可以访问如下链接。
原文链接:https://www.llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF
线程发起
回调函数 func(a)
传递给回调函数的参数 a
std::thread t1(func, a)
线程等待
std::thread t1(func, a)
在其它线程中调用,其它线程等待t1线程执行完毕
t1.join()
仿函数作为参数时
class test
{
public:
void operator()(std::string str){
std::cout << "str is " << str << std::endl;
}
};
错误调用
std::thread t2(test());
t2.join()
定义时没错,但是调用join时编译器会识别错误
错误原因
因为编译器会将t2当成一个函数对象, 返回一个std::thread类型的值, 函数的参数为一个函数指针,该函数指针返回值为background_task, 参数为void。可以理解为如下
"std::thread (*)(background_task (*)())"
修改方式
//可多加一层()
std::thread t2((background_task()));
t2.join();
//可使用{}方式初始化 C++11特性
std::thread t3{ background_task() };
t3.join();
Lambda表达式作为参数
std::thread t4([](std::string str) {
std::cout << "str is " << str << std::endl;
}, hellostr);
t4.join();
分离线程
线程允许采用分离的方式在后台独自运行,C++ concurrent programing书中称其为守护线程。
分离线程中可能存在的问题:局部变量提前释放
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();
}
// detach 注意事项
oops();
//防止主线程退出过快,需要停顿一下,让子线程跑起来detach
std::this_thread::sleep_for(std::chrono::seconds(1));
上面的例子存在隐患,因为some_local_state是局部变量, 当oops调用结束后局部变量some_local_state就可能被释放了,而线程还在detach后台运行,容易出现崩溃。
所以当我们在线程中使用局部变量的时候可以采取几个措施解决局部变量的问题
- 通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
- 将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
- 将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。
设置为等待线程,functhread执行完后,函数才释放,局部变量再释放
通过调用 functhread.join() 方法,主线程将等待 functhread 线程完成其执行。在 functhread 线程完成执行并且 join() 返回之后,局部变量(如 some_local_state)才会被销毁。
void use_join() {
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread(myfunc);
functhread.join();
}
// join 用法
use_join();
异常处理
主线程崩溃时,子线程也会异常退出,如果子线程处理的是一些重要信息或者任务,丢失这些信息很危险,所有采用异常捕捉,保证主线程崩溃时,子线程能够顺利执行完成再退出。
添加异常捕捉,主线程崩溃时,子线程也能顺利执行
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技术,保证线程对象析构的时候等待线程运行结束,回收资源。
线程守卫
class thread_guard {
private:
std::thread& _t;
public:
explicit thread_guard(std::thread& t):_t(t){}
~thread_guard() {
//join只能调用一次
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();
慎用隐式转换
引用参数
thread原理
绑定类成员函数
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();
}
注意:如果绑定的是普通函数,加不加&
都可以,编译器会默认将普通函数名作为函数地址,但是若绑定的是类中成员函数,必须加&
void thead_work1(std::string str) {
std::cout << "str is " << str << std::endl;
}
std::string hellostr = "hello world!";
//两种方式都正确
std::thread t1(thead_work1, hellostr);
std::thread t2(&thead_work1, hellostr);
使用move操作
有些资源为独占资源,不支持拷贝和赋值构造,所有通过move
将资源所有权进行转移。
void deal_unique(std::unique_ptr<int> p) {
std::cout << "unique ptr data is " << *p << std::endl;
(*p)++;
std::cout << "after unique ptr data is " << *p << std::endl;
}
void move_oops() {
auto p = std::make_unique<int>(100);
std::thread t(deal_unique, std::move(p));
t.join();
//不能再使用p了,p已经被move废弃
// std::cout << "after unique ptr data is " << *p << std::endl;
}