[TOC]

文章参考:https://www.runoob.com/w3cnote/cpp-std-thread.html

文章参考:https://www.cnblogs.com/haippy/p/3236136.html

概述

C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。

  • <atomic>:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
  • <thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
  • <mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
  • :该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

本文我们主要学习一下thread。

std::thread 在 <thread> 头文件中声明,因此使用 std::thread 时需要包含 <thread> 头文件。

thread 构造函数

构造函数 备注
thread() noexcept; 无参构造函数
template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args);
初始化构造函数
thread(const thread&) = delete; 拷贝构造函数 [deleted]
thread(thread&& x) noexcept; Move 构造函数
  • (1). 默认构造函数,创建一个空的 thread 执行对象。
  • (2). 初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
  • (3). 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
  • (4). move 构造函数,move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。

注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.

thread示例

下面是一个最简单的使用 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
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread> // std::thread

void thread_task() {
std::cout << "hello thread" << std::endl;
}

/*
* === FUNCTION =========================================================
* Name: main
* Description: program entry routine.
* ========================================================================
*/
int main(int argc, const char *argv[])
{
std::thread t(thread_task);
t.join();

return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */

Makefile 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
all:Thread

CC=g++
CPPFLAGS=-Wall -std=c++11 -ggdb
LDFLAGS=-pthread

Thread:Thread.o
$(CC) $(LDFLAGS) -o $@ $^

Thread.o:Thread.cc
$(CC) $(CPPFLAGS) -o $@ -c $^


.PHONY:
clean

clean:
rm Thread.o Thread

注意在 Linux GCC4.6 环境下,编译时需要加 -pthread,否则执行时会出现:

1
2
3
4
$ ./Thread
terminate called after throwing an instance of 'std::system_error'
what(): Operation not permitted
Aborted (core dumped)

原因是 GCC 默认没有加载 pthread 库,据说在后续的版本中可以不用在编译时添加 -pthread 选项。

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
35
36
37
38
39
40
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>

void func1(int n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << n << " executing\n";
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

void func2(int& n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 2 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main()
{
int n = 0;
// 默认构造函数,创建一个空的 thread 执行对象。
std::thread t1; // t1 is not a thread
// 初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,
// 新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
std::thread t2(func1, n + 1); // pass by value
// 创建一个 thread对象 会调用func2 传入的参数是std::ref(n)
std::thread t3(func2, std::ref(n)); // pass by reference
// move 构造函数,move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
std::thread t4(std::move(t3)); // t4 is now running f2(). t3 is no longer a thread
t2.join();
t4.join();
std::cout << "Final value of n is " << n << '\n';
}

设置线程名

长期在linux 中开发的同学,这两个函数应该是非常熟悉的。prctl 的功能十分强大,但对于设置线程名称单独的特性而言,pthread_setname_np 更是灵活些。

prctl

1
2
3
#include <sys/prctl.h>

int prctl(int option, unsigned long arg2, unsigned long arg3,unsigned long arg4, unsigned long arg5);

prctl 的option 太多了,功能十分强大,这里只针对线程名。

pthread_setname_np

这个函数比较相比较与prctl 就纯粹了,只是用于设置线程的名称。但是,不同的是接口是设置指定线程的线程名。

1
2
3
4
5
#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <pthread.h>
int pthread_setname_np(pthread_t thread, const char *name);
int pthread_getname_np(pthread_t thread,
char *name, size_t len);

第一个参数:需要设置/获取 名称的线程;

第二个参数:要设置/获取 名称的buffer;

同样的,要求name 的buffer 空间不能超过16个字节,不然会报错 ERANGE

详细的可以看下描述:

By default, all the threads created using pthread_create() inherit the program name. The pthread_setname_np() function can be used to set a unique name for a
thread, which can be useful for debugging multithreaded applications. The thread name is a meaningful C language string, whose length is restricted to 16 char‐
acters, including the terminating null byte (‘\0’). The thread argument specifies the thread whose name is to be changed; name specifies the new name.

The pthread_getname_np() function can be used to retrieve the name of the thread. The thread argument specifies the thread whose name is to be retrieved. The
buffer name is used to return the thread name; len specifies the number of bytes available in name. The buffer specified by name should be at least 16 charac‐
ters in length. The returned thread name in the output buffer will be null terminated.

大致意思:

通过pthread_setname_np 给一个线程设置唯一名称,这样对于多线程开发中很有意义。要求名称不超16字节,包括结尾的\0。

通过pthread_getname_np 能够获取一个线程的名称,参数的buffer 用以接受name,要求这个buffer 最少要有16个字节的空间。如果buffer 空间小于thread name,最终会返回null。

等待线程结束

C++11有两种方式来等待线程结束:

detach方式

启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。

  • 调用detach表示thread对象和其表示的线程完全分离;
  • 分离之后的线程是不在受约束和管制,会单独执行,直到执行完毕释放资源,可以看做是一个daemon线程;
  • 分离之后thread对象不再表示任何线程;
  • 分离之后joinable() == false,即使还在执行;

join方式

等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。

  • 只有处于活动状态线程才能调用join,可以通过joinable()函数检查;
  • joinable() == true表示当前线程是活动线程,才可以调用join函数;
  • 默认构造函数创建的对象是joinable() == false;
  • join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;
  • 调用 ternimate()的线程必须是 joinable() == false;
  • 如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

其他成员函数

函数名称 函数说明 备注
get_id 获取线程ID
joinable 检查线程是否被join
detach Detach线程
swap Swap线程
native_handle 返回 native handle
hardware_concurrency [static] 检测硬件并发特性。