1.线程库
linux中有pthread动态库,几乎所有的linux平台都自带这个动态库,所以在makefil编译的时候需要链接这个动态库。
cpp
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f mythead
2.pthread_create创建线程


pthread_t:一个长整型类型。
参数thread:用来返回线程的tid,也就类似于进程的pid。
参数attr:是一个const pthread_attr_t *类型的,作用是为线程设置属性,这里我们不需要给线程设置属性,所以默认我们不关心第二个参数attr,即传入nullptr即可
参数start_routine :即一个函数指针类型void *(*) (void *)类型的,用于传入我们要让线程执行的函数,也就意味着这个函数的类型需要是**void*(void*),**因为我们创建线程的目的就是让线程执行函数任务
参数arg:是arg类型是void*,用于给线程执行的函数传参,这里我们暂时不关心设置为nullptr即可
pthread_create的返回值,如果创建新线程成功,那么返回0,并且新线程的tid被定义,如果创建新线程失败,那么返回错误码,并且新线程的tid没有被定义,这里有一个值得思考的点,为什么创建新线程失败,并没有设置错误码errno呢?
因为一个进程中可能有多个线程,而错误码errno是一个全局变量只有一个,所以并不能支持所有的新线程设置错误码,所以干脆新线程如果出错了都不设置错误码了,而是直接返回错误码即可,这样就保证了每一个线程都可以有自己的错误码。
代码1示例:创建一个线程去一直打印进程的pid,主线程也一直打印进程pid。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
while(true)
{
cout << "I am a thread, pid: " << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
while(true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:

1.可以看到打印的pid是一样的,说明处于同一个进程中。
2.同时可以看出来,打印出来的结果并不是主线程打印一行,线程打印一行,而是有时候打印一行有时候打印两行,说明两个线程在争取显示器资源,后续可以通过加锁来实现互斥。
代码2示例:在代码1的基础上面,通过第四个参数传入参数给执行的函数
cpp
1 #include <iostream>
2 #include <pthread.h>
3 #include <unistd.h>
4
5 using namespace std;
6 //pthrtead_create
7 void* threadRoutine(void* args)
8 {
9 const char*str=static_cast<const char*>(args);
10 while(true)
11 {
12 cout<<str << ", I am a thread, pid: " << getpid() << endl;
13 sleep(1);
14 }
15 }
16
17 int main()
18 {
19 pthread_t tid;
20 pthread_create(&tid, nullptr, threadRoutine, (void*)"hello world");
21
22 while(true)
23 {
24 cout << "main thread, pid: " << getpid() << endl;
25 sleep(1);
26 }
27
28 return 0;
29 }
运行结果:

因为第四个参数是void*类型,所以在传入之前是需要强转为void*类型,在函数里面接收后,需要强转为原来的类型。
**代码3示例:**第四个参数不仅仅可以传入内置类型,也可以传入类对象。
cpp
31 struct ThreadData
32 {
33 public:
34 ThreadData(string name)
35 :threadname(name)
36 {}
37 string threadname;
38 };
39
40 void* threadRoutine(void* args)
41 {
42 ThreadData*str=static_cast<ThreadData*>(args);
43 while(true)
44 {
45 cout<<str->threadname << ", I am a thread, pid: " << getpid() << endl;
46 sleep(1);
47 }
48 }
49
50 int main()
51 {
52 pthread_t tid;
53 ThreadData td("hello worla");
54 pthread_create(&tid, nullptr, threadRoutine, (void*)&td);
55
56 while(true)
57 {
58 cout << "main thread, pid: " << getpid() << endl;
59 sleep(1);
60 }
61
62 return 0;
63 }
运行结果:

2.1 ps -aL指令

使用ps -aL可以查看系统中的全部的轻量级进程,其中-aL选项中的a是all全部的意思,L是轻量级进程的意思,所以当左侧进程运行起来之后,我们使用ps -aL查看系统中全部的轻量级进程。
图中PID代表的是进程的PID,LWP表示的light weight process轻量级进程的意思,这个LWP是给内核看的,主线程的LWP等于进程的PID,CPU可以通过比较LWP和PID来区分是否为主线程,如果主线程的时间片到了,就需要进程切换。
3.pthread_wait线程等待
创建线程起始就是创建一个PCB对象,所以一个线程也需要被等待,不然也会造成内存泄漏。
(1)回收新线程,防止内存泄漏(2)如果需要的话,获取新线程执行任务的返回值,确认新线程是否执行完任务。

参数thread:等待的线程tid
参数retval:二级指针,存储的是pthread_create第3个参数的返回值的地址,如果不想获取设为nullptr即可。
**代码示例1:**不关心函数的返回值,创建一个线程,线程5秒后退出
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
const char* name = static_cast<const char*>(args);
int cnt = 5;
while(true)
{
cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
sleep(1);
cnt--;
if(cnt == 0)
break;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
pthread_join(tid, nullptr);
cout << "main thread quit..." << endl;
return 0;
}
运行结果:

可以看出来,pthread_join的等待方式是非阻塞等待方式,主线程结束了,不会退出,会阻塞的等待线程的退出。
**代码示例2:**使用pthread的第二个参数去获取函数的返回值
cpp
67 void* threadRoutine(void* args)
68 {
69 const char* name = static_cast<const char*>(args);
70 int cnt = 5;
71 while(true)
72 {
73 cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
74 sleep(1);
75
76 cnt--;
77 if(cnt == 0)
78 break;
79 }
80 return (void*)1;
81 }
82
83 int main()
84 {
85 pthread_t tid;
86 pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
87 void*retval;
88 pthread_join(tid,&retval);
89 cout << "main thread quit..." <<(int) retval<<endl;
90
91 return 0;
92 }
运行结果:

会发现编译不过去,因为在linux中,是64位的,指针的字节是8位,但是int是固定的4字节,就会触发指针转窄整数的报错,可以将int换位(long long int)是固定的8字节数。
修改后代码:
cpp
67 void* threadRoutine(void* args)
68 {
69 const char* name = static_cast<const char*>(args);
70 int cnt = 5;
71 while(true)
72 {
73 cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
74 sleep(1);
75
76 cnt--;
77 if(cnt == 0)
78 break;
79 }
80 return (void*)1;
81 }
82
83 int main()
84 {
85 pthread_t tid;
86 pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
87 void*retval;
88 pthread_join(tid,&retval);
89 cout << "main thread quit..." <<(long long int) retval<<endl;
90
91 return 0;
92 }
修改后编译结果:

所以这样子以后,主线程就可以获取线程的pthread_create的返回值(退出码),来知道线程的返回情况。那么对于退出码已经可以获取了,那么对于异常呢?
代码示例:线程发送/0错误,收到信号,主线程的退出情况。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
const char* name = static_cast<const char*>(args);
int cnt = 3;
while(true)
{
cout << name << ", new thread, pid: " << getpid() << ", cnt: " << cnt << endl;
sleep(1);
cnt--;
if(cnt == 0)
break;
}
int a = 10;
a /= 0;
return (void*)2;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
void* retval;
pthread_join(tid, &retval);
cout << "main thread quit..., ret: " << (long long int)retval << endl;
return 0;
}
运行结果:

信号其实是相对于进程发送的,所以当线程发送了异常,进程也就会得到了信号,直接退出了,也就打印不出来主线程的结束语句了。
小疑问:为什么函数的返回值要使用二级指针来获取,用一级指针不是就可以了吗?
因为pthread线程库是线程的管理者,线程属性中有线程函数,那么线程函数的返回值pthread线程库也会知道,所以线程函数的返回值被存储到了pthread线程库的一个区域中,此时这个区域中存储的是一个void*的一级指针,那么pthread_join接口想要获取这个区域的一级指针只能采用二级指针来进行获取。
4.线程进一步理解
4.1 全局变量
进程地址空间里面的大部分数据都是被共享的,那么创建一个全局变量,是不是会被多个线程共享呢?
代码示例1:主线程每隔2秒打印一次g_val的值,并对g_val的值++,线程每隔一秒打印一次g_val的值。
cpp
128 int g_val = 100;
129
130 void* threadRoutine(void* args)
131 {
132 const char* name = static_cast<const char*>(args);
133
134 while(true)
135 {
136 printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
137 sleep(1);
138 }
139
140 return (void*)0;
141 }
142
143 int main()
144 {
145 pthread_t tid;
146 pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
147
148 while(true)
149 {
150 printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
151 g_val++;
152 sleep(2);
153 }
154
155 void* retval;
156 pthread_join(tid, &retval);
157 cout<<g_val<<endl;
158 cout << "main thread quit..., ret: " << (long long int)retval << endl;
159
160 return 0;
161 }
运行结果:

可以看到每次主线程对g_val的值++,线程访问到的g_val的值也随之改变,并且每次访问的地址都相同。
**代码示例2:**使用__thread来修饰变量,会使每一个线程中都有一个独立的g_val变量,一个线程的修改不会影响其他线程。
cpp
128 __thread int g_val = 100;
129
130 void* threadRoutine(void* args)
131 {
132 const char* name = static_cast<const char*>(args);
133
134 while(true)
135 {
136 printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
137 sleep(1);
138 }
139
140 return (void*)0;
141 }
142
143 int main()
144 {
145 pthread_t tid;
146 pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
147
148 while(true)
149 {
150 printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
151 g_val++;
152 sleep(2);
153 }
154
155 void* retval;
156 pthread_join(tid, &retval);
157 cout<<g_val<<endl;
158 cout << "main thread quit..., ret: " << (long long int)retval << endl;
159
160 return 0;
161 }
运行结果:

4.2 可重入函数
可重入函数指的是能够被多个执行流(如进程、线程、中断服务程序)安全地并发调用,且不会因为共享资源或自身状态导致结果错误的函数。
代码示例:
cpp
165 void show(const string& name)
166 {
167 cout << name << "say# " << "hello thread" << endl;
168 }
169
170 void* threadRoutine(void* args)
171 {
172 while(true)
173 {
174 show("new thread");
175
176 sleep(1);
177 }
178
179 return (void*)0;
180 }
181
182 int main()
183 {
184 pthread_t tid;
185 pthread_create(&tid, nullptr, threadRoutine, nullptr);
186
187 while(true)
188 {
189 show("main thread");
190
191 sleep(1);
192 }
193
194 void* retval;
195 pthread_join(tid, &retval);
196 cout << "main thread quit..., ret: " << (long long int)retval << endl;
197
198 return 0;
199 }
运行结果:

5. pthread_excl和pthread_cancel线程终止
线程的终止方法有3个
1.return返回
2.pthread_exit返回
3.pthread_cancel返回
5.1 pthread_excl

参数retval:线程的返回值。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
const char* name = static_cast<const char*>(args);
int cnt = 3;
while(true)
{
cout << name << ", new thread, pid: " << getpid() << ", cnt: " << cnt << endl;
sleep(1);
cnt--;
if(cnt == 0)
break;
}
pthread_exit((void*)7);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
void* retval;
pthread_join(tid, &retval);
cout << "main thread quit..., ret: " << (long long int)retval << endl;
return 0;
}
运行结果:

ps:这里要和exit区分开,如果线程里面调用的是exit,是把整个进程都退出了。
5.2 pthread_cancel

参数thread:需要传入线程的tid。
pthread_cancel同样也是一种终止单个线程的方法,但是pthread_cancel并不常用,pthread_cancel的参数是传入要取消的线程的tid即可去掉指定线程。
**代码示例:**在主线程创建出新线程后,新线程执行死循环打印的线程函数,在主线程中,当新线程运行2秒后,在主线程中使用pthread_cancel取消新线程,观察新线程能否被取消,并且在主线程中观察新线程的退出码
cpp
233 void* threadRoutine(void* args)
234 {
235 const char* name = static_cast<const char*>(args);
236
237 while(true)
238 {
239 cout << name << ", new thread, pid: " << getpid() << endl;
240 sleep(1);
241 }
242 }
243
244 int main()
245 {
246 pthread_t tid;
247 pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
248
249 sleep(3);
250
251 pthread_cancel(tid);
252
253 void* retval;
254 pthread_join(tid, &retval);
255 cout << "main thread quit..., ret: " << (long long int)retval << endl;
256
257 return 0;
258 }
运行结果:

新线程虽然执行的是死循环的线程函数,但是2秒过后,只要主线程中调用的pthread_cancel对新线程进行了取消,那么新线程无论在做什么都会被立即去掉,进而终止新线程,一旦新线程被终止,主线程自热而然就可以等待成功新线程,获取新线程的退出码为-1

并且这个-1实际上是一个专门用于线程被pthread_cancel取消后返回的宏定义也就是PTHREAD_CANCELED,即(void*)-1
也就是相对于pthread_excl是线程自己来退出,pthread_cancel而是主线程来决定线程的退出。
6.pthread_self获取进程的tid
首先要注意LWP和tid是两个不同的东西,LWP是操作系统用来管理进程的。

代码示例:主线程和线程都打印线程的tid。
cpp
#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <stdlib.h>
using namespace std;
string ToHex(pthread_t tid)
{
char hex[64];
snprintf(hex, sizeof(hex), "%p", tid);
return hex;
}
void* threadRoutine(void* args)
{
cout << "new thread running, tid: " << ToHex(pthread_self()) << endl;
return (void*)0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
cout << "main thread create thread done, new thread tid: " << ToHex(tid) << endl;
pthread_join(tid, nullptr);
return 0;
}
运行结果:

可以看出主线程打印的tid和线程打印的是一样的。
6.1 细说tid
上面对代码中,把tid转化位了16进程任何打印了出来,为什么呢?
首先要知道在内核中是没有线程的概念的,只有轻量化进程的概念,所以内核提供的系统接口是

这个接口实在是太复杂了,所以linux开发者编写了一个pthread库专门用于对于线程的操作,在底层是封装了clone函数,在linux中,编写多线程的代码,都需要使用gcc,g++编译链接时需要在后面加上-pthread来使用第三方库。
但是内核的角度是没有形成的概念的,只有轻量化进程概念,我们使用的
cpp
ps -aL
会显示所有的轻量化进程,查询到的LWP,操作系统也只认识这个东西,内核会记录每个LWP的运行状态(如运行、就绪、阻塞)、优先级、时间片等,以便决定下一个运行哪个LWP。
但是线程也是有很多属性的,像pthread_create创建线程,pthread_self()获取线程的tid,这些接口是用户态来调用的,这些数据都是需要被维护起来,给用户来获取的,所以线程库就需要去维护这些数据。

线程库是一个动态库,是一定会被加载到内存里面的,tid就指向存储这些数据属性的起始地址,所以我们在pthread_create创建线程时,就会返回tid这个地址给用户,来方便用户获取数据。
所以对于一个线程来说,PCB的数据会被存储于内核空间中,比如线程的时间片,LWP线程唯一标识符,还有一些线程的属性是存在于pthread库中的。
ps:**一定注意!!!**pthread_ create函数会产生一个线程ID(tid),存放在第一个参数指向的地址中。该线程ID和前面说的线程ID(LWP)不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
每一个线程都有自己的独立栈
因为每一个线程都有自己的独立链,主线程需要去调用函数去创建线程,等待线程,那么其他线程里面也有自己的独立链,可能去调用其他函数,就需要存储一些临时变量和函数执行过程中产生的临时数据,记录函数的调用关系,像返回的地址,参数,这个时候需要记录这些数据就需要栈空间来存储,这个栈空间在哪里呢?pthread库中
在线程中,因为主线程是在程序运行的时候就分配了栈区,所以主线程的栈是在地址空间的栈上,由于新线程的独立栈都是在程序运行期间被分配的,申请的栈空间都会存在于pthread库中申请。