介绍线程的基本管控,包括线程的发起,等待,异常条件下如何等待, 后台运行,线程的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!";
//1 通过()初始化并启动一个线程
std::thread t1(thead_work1, hellostr);
//2 主线程等待子线程退出
t1.join();

线程分离

线程允许在后台独自运行,可能会出现可预期的错误

  1. _i 绑定到 some_local_state 的引用(内存地址)
  2. 线程内部通过 _i 访问 some_local_state 的内存
  3. some_local_state 在 oops() 退出时被销毁,_i 成为悬空引用
  4. 访问已释放的内存 → 未定义行为(崩溃或数据错误)
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;// _i 是一个引用,直接绑定到 some_local_state(没有拷贝值)
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));

如何解决?

  1. 通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
  2. 将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
  3. 将线程运行的方式修改为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() {
//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();

慎用隐式转换

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绑定的回调函数是普通函数,可以在函数前加**&**或者不加**&**,因为编译器默认将普通函数名作为函数地址