C++并发编程之线程管理

C++11 引入的一个标准库类std::thread,用于创建和管理线程。它提供了一种方便的方式来执行并发任务,使得多线程编程变得更加简单和安全。

线程管理的基础

每个程序运行时都会有一个线程,即执行main函数的线程。

启动线程

通过std::thread对象来创建线程,线程在创建时启动,可以选择阻塞join或者分离detach执行。

这是一段启动线程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "bits/stdc++.h"
#include <thread> // 引入线程头文件

using namespace std;

void hello() {
cout << "Hello Concurrent World" << endl;
}

int main (int argc, char *argv[]) {
thread t{hello}; // 开启线程
t.join(); // 等待线程结束
// t.detach() // 分离线程
return 0;
}

如果线程对象t在析构时还未选择阻塞join或者分离detachstd::thread的析构函数会调用std::terminate(),会报以下错误:

1
2
terminate called without an active exception
Aborted

如果主线程结束时子线程还未结束,那么子线程也会被迫结束,例如以下例子是没有输出的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "bits/stdc++.h"
#include <thread>

using namespace std;

void hello() {
this_thread::sleep_for(chrono::seconds(1));
cout << "Hello Concurrent World" << endl;
}

int main (int argc, char *argv[]) {
thread t{hello};
t.detach();
return 0;
}

线程的分离

使用detach会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。

线程分离detach之后,只要主线程还在运行,线程就不会终止,而是会继续执行直到正常退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "bits/stdc++.h"
#include <thread>

using namespace std;

void hello() {
this_thread::sleep_for(chrono::seconds(1));
cout << "Hello Concurrent World" << endl;
}

void test_detach() {
thread t{hello};
t.detach();
}

int main (int argc, char *argv[]) {
test_detach();
this_thread::sleep_for(chrono::seconds(2));
return 0;
}

线程的分离需要特别注意变量的生命周期,避免线程依旧访问已经析构的变量

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
26
27
28
29
30
31
32
#include "bits/stdc++.h"
#include <thread>

using namespace std;

struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
i++; // 1. 潜在访问隐患:悬空引用
cout << "i = " << i << endl;
}
}
};

void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行

int main (int argc, char *argv[]) {
oops();
this_thread::sleep_for(10ms);
return 0;
}

上面这个例子可以看出,多线程对内存管理有着更高的要求,在使用引用传递和指针传递时需要格外谨慎。处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。

线程的等待

一般来说,线程的等待要比线程的分离更加难以处理,因为线程的分离可以在线程创建时就执行,而线程的等待需要精心挑选一个位置。因为调用join会让当前线程进入阻塞,无法执行其他任务,导致多线程的收益减弱。

除此之外,还可以使用std::thread::joinable来判断线程是否可以调用join,如果线程已经被join、detach或者已经结束了,则返回False

线程管理实践

一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用**join()**,如同下面清单中的代码。看它如何简化f()函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "bits/stdc++.h"
#include <thread>

using namespace std;

struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
i++; // 1. 潜在访问隐患:悬空引用
cout << "i = " << i << endl;
}
}
};

class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):
t(t_)
{}
~thread_guard()
{
if(t.joinable()) // 1
{
t.join(); // 2
}
}
thread_guard(thread_guard const&)=delete; // 3
thread_guard& operator=(thread_guard const&)=delete;
};

void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
thread_guard my_guard(my_thread);
}

int main (int argc, char *argv[]) {
oops();
this_thread::sleep_for(10ms);
return 0;
}

当执行完oops函数时,会对my_guard对象进行析构,调用my_guard的析构函数。析构函数判断线程是否可join,并等待线程结束。

拷贝构造函数和拷贝赋值操作被标记为=delete,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

作者

echo

发布于

2024-06-27

更新于

2024-08-10

许可协议

评论