0%

C++多线程基础

线程

概述

线程(内核线程)是操作系统进行任务调度的基本单位,一般来说,操作系统以线程为单位进行处理机调度。

通常一个线程只具有控制流、处理机和栈等少量运行中必不可少资源,因此必须依靠进程提供相应的系统资源等才能正常运行。因此线程必须与进程绑定,当进程退出时,其内部的线程都将全部退出。一个进程可以有多个线程,它们共享该线程提供的系统资源等。

线程状态

线程存在许多状态,其转换图如下:

分离线程与可结合线程

Linux线程被分为分离线程与可结合线程,默认情况下创建的线程为可结合线程。两者的区别如下:

  1. 是否可以join:分离线程不可以使用join进行结合,可结合线程能够使用join进行结合。

  2. 资源释放时机:分离线程在线程运行结束后自动释放,可结合线程在调用join结束后释放。

CPP线程库

在C++11中引入了std::thread类用于创建和管理线程,并在std::this_thread命名空间中引入了一些方法用于更好的对当前线程进行控制。

std::thread

API

创建
方法 含义 备注
thread() noexcept; 默认构造函数 创建一个空线程对象,该对象不对应任何实际线程
template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args);
初始化构造函数 根据输入参数创建一个线程,该线程为可结合线程,并且创建后线程立即进入就绪状态。
线程中将会自动运行fn(args...)函数
thread(thread&& x) noexcept; 移动构造函数 只涉及线程控制权的转移,调用后新建的线程对象将能够控制输入对象原先管理的实际线程,输入对象变为空线程对象
移动
方法 含义
thread& operator=(thread&& rhs) noexcept; 移动赋值,仅涉及线程管理权的转移,使用后调用对象获取传入参数实际对应的实际线程的管理权,传入参数变为空对象。
void swap(std::thread& other) noexcept; 交换两个线程对象内实际管理线程的管理权。
状态获取
方法 含义
bool joinable() const noexcept; 返回是否为活动进程,通常相当于get_id()!=std::thread::id ()
std::thread::id get_id() const noexcept; 返回线程id,线程id在操作系统范围内是唯一的。
static unsigned int hardware_concurrency() noexcept; 返回硬件支持的最大并行线程数。对于计算密集型任务,通常用于进行参考。
线程管理
方法 含义
void join(); join该对象管理的实际线程,调用该方法的线程将被阻塞直到对应线程对象运行完成且资源完成回收。
Linux下,可结合线程只有在调用join后才会释放栈等相关资源。
若在不是可结合线程的线程对象上调用该方法将抛出std::system_error。
调用结束后原线程对象变为空对象。
void detach(); detach相应的线程,调用该方法后该对象管理的线程将处于分离状态。
Linux中,分离线程在程序运行结束后将会立即释放资源。
若在不是可结合线程的线程对象上调用该方法将抛出std::system_error。
调用结束后原线程对象变为空对象。

参数

函数参数传递

对函数而言,共有4种参数:

  1. 值参数

  2. 左值引用

  3. 常量左值引用

  4. 右值引用

同时有两种参数传递方式:

  1. 值传递:在调用函数时创建对应副本,可以被分为初始化构造、拷贝构造与移动构造。

  2. 引用传递:不发生拷贝,类似传入指针。

测试代码与执行结果

使用如下代码进行测试,其中被注释的部分表示会导致编译错误:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <iostream>
#include <string>
#include <thread>

class TestClass {
public:
TestClass(const std::string &s) : str(s) {
print("DCtor");
}

TestClass(const TestClass &t) : str("Copy_" + t.str) {
print("CCtor");
}

TestClass(TestClass &&t) : str("Move_" + t.str) {
print("MCtor");
}

TestClass &operator=(const TestClass &t) {
str = "CopyA_" + t.str;
print("CAssign");
return *this;
}

TestClass &operator=(TestClass &&t) {
str = "MoveA_" + t.str;
print("MAssign");
return *this;
}

~TestClass() {
print("Dtor");
}

friend std::ostream &operator<<(std::ostream &out, const TestClass &tc) {
return out << "Print: " << tc.str;
}
private:
void print(const char *s) {
std::cout << s << ": " << str << std::endl;
}
private:
std::string str;
};

void func1(TestClass a) {
std::cout << a << std::endl;

}

void func2(TestClass &b) {
std::cout << b << std::endl;
}

void func3(const TestClass &c) {
std::cout << c << std::endl;
}

void func4(TestClass &&d) {
std::cout << d << std::endl;
}

void test1() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func2, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func1, t).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::ref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::cref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}

void test2() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func2, __func__).join();
// std::cout << "==============================" << std::endl;
// std::thread(func2, t).join();
// std::cout << "==============================" << std::endl;
std::thread(func2, std::ref(t)).join();
std::cout << "==============================" << std::endl;
// std::thread(func2, std::cref(t)).join();
// std::cout << "==============================" << std::endl;
// std::thread(func2, std::move(t)).join();
// std::cout << __func__ << '-' << std::endl;
}

void test3() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func3, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func3, t).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::ref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::cref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}

void test4() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func4, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func4, t).join();
std::cout << "==============================" << std::endl;
// std::thread(func4, std::ref(t)).join();
// std::cout << "==============================" << std::endl;
// std::thread(func4, std::cref(t)).join();
// std::cout << "==============================" << std::endl;
std::thread(func4, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}


int main(int argc, char *argv[]) {
test1();
std::cout << std::endl;
test2();
std::cout << std::endl;
test3();
std::cout << std::endl;
test4();
return 0;
}

使用如下命令进行编译运行:

1
g++ test.cpp -o test -std=c++17

结果如下:

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
51
52
53
54
55
56
57
DCtor: test1
test1+
CCtor: Copy_test1
MCtor: Move_Copy_test1
Print: Move_Copy_test1
Dtor: Move_Copy_test1
Dtor: Copy_test1
==============================
CCtor: Copy_test1
Print: Copy_test1
Dtor: Copy_test1
==============================
CCtor: Copy_test1
Print: Copy_test1
Dtor: Copy_test1
==============================
MCtor: Move_test1
MCtor: Move_Move_test1
Print: Move_Move_test1
Dtor: Move_Move_test1
Dtor: Move_test1
test1-
Dtor: test1

DCtor: test2
test2+
Print: test2
==============================
Dtor: test2

DCtor: test3
test3+
CCtor: Copy_test3
Print: Copy_test3
Dtor: Copy_test3
==============================
Print: test3
==============================
Print: test3
==============================
MCtor: Move_test3
Print: Move_test3
Dtor: Move_test3
test3-
Dtor: test3

DCtor: test4
test4+
CCtor: Copy_test4
Print: Copy_test4
Dtor: Copy_test4
==============================
MCtor: Move_test4
Print: Move_test4
Dtor: Move_test4
test4-
Dtor: test4
分析

实际上,在调用初始化构造函数创建一个实际线程时需要进行三次次参数传递,分别是:

  1. 从调用函数传递到构造函数中。在这一步中将会使用万能引用,因此全是引用传递。

  2. 在构造函数函数中在堆上建立相应的_Invoker对象,传递到新线程中。在这一步中将会创建对应变量,其传递方式由目标函数参数的种类和传入方式决定,其具体传递方法如下表:

    T T& const T& T&&
    t 拷贝 拷贝 拷贝
    std::ref(t) 拷贝 引用 引用
    std::cref(t) 拷贝 引用
    std::move(t) 移动 移动 移动
  3. 在新线程中调用目标函数。在这一步中将会采用目标函数对应参数的种类决定传递方式,其中值传递将会以移动形式调用,引用传递将使用进行引用传递。

std::this_thread

方法

std::this_thread命名空间中引入了一些方法,这些方法只作用于调用改代码的线程,用于更好的控制线程运行。这些方法如下:

方法 含义
void yield() noexcept; 请求操作系统进行新一轮的线程调度,在希望当前运行线程让出处理器时调用。
std::thread::id get_id() noexcept; 返回当前线程的线程id。
template <class Rep, class Period>
void sleep_for(const std::chrono::duration<Rep, Period>& sleep_duration);
当前线程休眠一段时间。通常情况下是当前线程进入阻塞状态sleep_duration,随后进入就绪状态等待运行。
由于存在调度延时等,在时间上可能存在较大误差。
template <class Clock, class Duration>
void sleep_until(const std::chrono::time_point<Clock, Duration>& sleep_time);
当前线程休眠直到指定时间。通常情况下是当前线程进入阻塞状态直到sleep_time,随后进入就绪状态等待运行。
由于存在调度延时等,在时间上可能存在较大误差。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
#include <chrono>

using namespace std::chrono_literals;

void func() {
auto id = std::this_thread::get_id();
std::cout << id << std::endl;
std::this_thread::sleep_for(2000ms);
std::this_thread::sleep_until(std::chrono::steady_clock::now() + 2000ms);std::this_thread::yield();
}

int main(int argc, char *argv[]) {
std::thread(func).join();
return 0;
}