0%

C++ 中 std::thread 的基本用法

C++11 中引入了 std::thread 可以比较方便的创建和管理多线程,这篇笔记主要简单记录了一下我的学习过程。包括线程的创建的管理还有在类中相关的用法。

requirement

为了使用 std::thread 我们需要添加 <thread> 作为头文件,同时如果使用 cmake 进行项目编译管理的话,需要添加以下两行进行相关库的链接,然后就可以使用:

1
2
3
4
5
6
7
find_package (Threads)

...
add your executable
...

target_link_libraries (your_project_name ${CMAKE_THREAD_LIBS_INIT})

basic usage

std::thread 的使用比较简单,直接通过构造函数可以创建一个线程,有需要的话也可以传入参数,见以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 无参数函数
void foo () {
std::cout << "A thread function!" << std::endl;
}

// 有参数函数
void fooWithName (std::string func_name) {
std::cout << "A thread function with name: " << func_name << "!" << std::endl;
}

int main() {

std::thread t1(foo);
t1.join();

std::thread t2(fooWithName, "FuncName");
t2.join();

return 0;
}}

输出:

1
2
A thread function!
A thread function with name: FuncName!!

join() vs detach()

可以看到,上面代码例子中创建的两个线程后都增加了一条 join() 方法,这是因为通过 std::thread 创建的线程默认下并不是独立于主线程 (main())的,如果不加 join() 的话,当主线程退出之后子线程也会报错并退出,见如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void fooWithTime(int n_seconds, std::string name) {
for (int i = 0; i < n_seconds; i++) {
usleep(1000);
std::cout << name <<" Thread: worked for " << i + 1 << " seconds.." << std::endl;
}
}

int main() {

std::thread t1(fooWithTime, 5, "Child");

fooWithTime(3, "Main");

t1.join();

std::cout << "Main thread exit! " << std::endl;

return 0;
}}

由于在 main 函数最后加入 t1.join() 所以在主函数输出 3 秒之后会等到 子线程工作完返回时才退出,输出如下:

1
2
3
4
5
6
7
8
9
Main Thread: worked for 1 seconds..
Child Thread: worked for 1 seconds..
Main Thread: worked for 2 seconds..
Child Thread: worked for 2 seconds..
Main Thread: worked for 3 seconds..
Child Thread: worked for 3 seconds..
Child Thread: worked for 4 seconds..
Child Thread: worked for 5 seconds..
Main thread exit!

如果把 t1.join() 去掉之后,输出如下:

1
2
3
4
5
6
7
8
Main Thread: worked for 1 seconds..
Child Thread: worked for 1 seconds..
Child Thread: worked for 2 seconds..
Main Thread: worked for 2 seconds..
Child Thread: worked for 3 seconds..
Main Thread: worked for 3 seconds..
Main thread exit!
terminate called without an active exceptionn

可以看到在主线程退出之后,子线程也被强制退出了,如果想让子线程独立于主线程的话,需要加入detach()

代码和输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void fooWithTime(int n_seconds, std::string name) {
for (int i = 0; i < n_seconds; i++) {
usleep(1000);
std::cout << name <<" Thread: worked for " << i + 1 << " seconds.." << std::endl;
}
}

int main() {

std::thread t1(fooWithTime, 5, "Child");
t1.detach();

fooWithTime(3, "Main");


std::cout << "Main thread exit! " << std::endl;

return 0;
}
1
2
3
4
5
6
Main Thread: worked for 1 seconds..
Child Thread: worked for 1 seconds..
Main Thread: worked for 2 seconds..
Child Thread: worked for 2 seconds..
Main Thread: worked for 3 seconds..
Main thread exit!

可以发现,我们虽然 detach 了子线程,但是从输出上来看在主线程退出之后子线程也没有输出了,这是因为在子线程 detach 了之后,主线程退出的同时主进程也同时退出了,而我们运行进程时只能看到该进程的输出,所以就看不到 detach 后的线程的输出了。值得注意的是一旦线程被detach 之后就不能再进行 join 操作了,所以对 detach 的使用需要谨慎一点,并且在对一个线程进行 join 之前,应该通过 joinable() 进行判断。如下所示:

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
void fooWithTime(int n_seconds, std::string name) {
for (int i = 0; i < n_seconds; i++) {
usleep(1000);
std::cout << name <<" Thread: worked for " << i + 1 << " seconds.." << std::endl;
}
}

int main() {

std::thread t1(fooWithTime, 5, "Child");
t1.detach();

if (t1.joinable()) {
t1.join();
} else {
std::cout << "Thread unjoinable!" << std::endl;
}

fooWithTime(3, "Main");


std::cout << "Main thread exit! " << std::endl;

return 0;
}

threads with class

在实际使用中,我们的项目通过使用了各种类,下面代码演示了如何把类和 std::thread 结合使用:

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
class A {
public:
std::string class_param;

void funcWithoutParam() {
std::cout << "non-static func: class param: " << class_param << std::endl;
}

void funcWithParam(std::string external_param) {
std::cout << "non-static func: class param: " << class_param << ", external param: " << external_param << std::endl;
}

static void static_func(std::string param) {
std::cout << "static func: param: " << param << std::endl;
}
};

int main() {
A a;
a.class_param = "Apple";

std::thread t1(&A::funcWithoutParam, a);
t1.join();

std::thread t2(&A::funcWithParam, a, "banana");
t2.join();

std::thread t3(&A::static_func, "Cherry");
t3.join();

std::cout << "Main thread exit! " << std::endl;

return 0;
}

输出:

1
2
3
4
non-static func: class param: Apple
non-static func: class param: Apple, external param: banana
static func: param: Cherry
Main thread exit!

上面包含了三种用法:

  • 如果传入线程的函数是类中的 static 函数,可以直接传入函数指针和相关的参数,主要这里的函数比如显示地用 & 进行传入;
  • 如果传入线程的函数是 non-static 函数,我们还必须要传入该类的某个实例,并且这样传入的线程可以调用该实例所有变量,构造函数顺序是 类函数,实例,参数1, 参数2, ...

conclusion

这篇笔记简单概括了一下 std::thread,值得注意的是,上面的所有例子都直接最简单的应用,并没有涉及资源分配的情况,在实际使用需要注意不同线程的对同一资源的使用 (race_condition),避免出现死锁。