【Hello Linux】线程控制
作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:简单介绍linux中的线程控制
线程控制
- 线程创建
- 线程等待
- 线程终止
- 线程分离
- 线程id和进程地址空间布局
线程创建
我们可以通过下面pthread_create函数
来创建一个新的线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值说明:
- 线程创建成功返回0 失败返回错误码
这里值得注意的是在线程库中 几乎所有的返回值都是成功返回0 失败返回错误码
参数说明:
- thread:获取创建成功的线程ID 该参数是一个输出型参数
- attr: 用于设置创建线程的属性 如果我们传入NULL则设置为默认属性
- start_routine:这是一个函数地址 传入我们想要这个线程执行的函数
- arg: 传给线程例程的参数 (默认是void* 类型 记得类型强转不然会报警告)
下面是代码示例
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
6 void* run_thread(void* args)
7 {
W> 8 char* msg = (char*)args;
9 while(1)
10 {
11 printf("im a new pthread my tid is: %lu\n" , pthread_self());
12 sleep(1);
13 }
14 }
15
16
17 int main()
18 {
19 pthread_t tid;
20 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
21 while(1)
22 {
23 printf("im main thread i create the new pid is:%lu\n", tid);
24 sleep(1);
25 }
26 return 0;
27 }
上面代码的意思是我们创建一个子线程 这个线程不停的打印参数发送过去的消息 同时我们的主进程不停的打印另外的信息 同时我们调用了一个叫做pthread_self()
的函数
但是这里我们发现我们进行编译之后会出现这样子的情况 这是因为我们找不到库文件所导致的
具体的原因可以参考我动静态库这篇博客
动静态库
所以说我们想要编译成功这个文件需要先指令连接的库文件
演示效果如下
我们可以发现新线程的pid就是我们主进程创建的pid
并且这个程序在同时执行两个死循环 这在我们之前的单执行流程序中是很难做到的
在这里我们还可以演示下线程健壮性不够强的特点 具体思路如下
我们在创建的新线程的代码中休眠三秒钟 并且故意造成一个野指针越界的问题
之后看看主进程会不会崩溃
具体代码表示如下
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
6 void* run_thread(void* args)
7 {
W> 8 char* msg = (char*)args;
9 while(1)
10 {
11 printf("im a new pthread my tid is: %lu\n" , pthread_self());
12 sleep(3);
13 int* p = NULL;
14 *p = 20;
15 }
16 }
17
18
19 int main()
20 {
21 pthread_t tid;
22 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
23 while(1)
24 {
25 printf("im main thread i create the new pid is:%lu\n", tid);
26 sleep(1);
27 }
28 return 0;
29 }
线程等待
我们在学进程控制的时候讲过要进行进程等待 不然的话会造成僵尸进程的问题
同样的我们的线程也是通过创建PCB来保存数据 所以说线程也需要进程线程等待 不然的话会造成类似于僵尸线程的问题
在Linux操作系统中我们可以使用pthread_join
函数来进行线程等待
pthread_join函数的函数原型如下:
int pthread_join(pthread_t thread, void **retval);
返回值说明:
- 线程等待成功返回0 失败返回错误码
参数说明
- thread:被等待线程的ID
- retval:线程退出时的退出码信息 其中因为线程退出的返回值是一个void*类型的数据 所以我们要使用一个void * *类型的数据去接受它
调用该函数的线程将挂起等待 直到ID为thread的线程终止
下面是代码演示
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
W> 6 void* run_thread(void* args)
7 {
8 printf("im new thread my tid is:%lu\n",pthread_self());
9 sleep(3);
10 return (void*)101;
11 }
12
13
14 int main()
15 {
16 pthread_t tid;
17 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
18 void* status = NULL;
19 printf("waiting ...\n");
20 pthread_join(tid ,&status);
21 printf("wait success , exit code is:%d\n", (int)(intptr_t)status);
22 return 0;
23 }
解释下上面这段代码
我们首先创建一个新线程 这个线程会在打印自己的线程tid之后休眠三秒并结束
最后我们主线程使用join函数接受它的返回值并打印
这里需要注意的是 我们并不能直接打印status 而是要将它强制类型转化为intptr_t
再强制转化为int
演示结果如下
我们发现join函数确实可以接收我们的返回值
异常情况需要处理吗?
我们都知道一段程序运行有三种情况
- 运行成功 结果正确返回退出码
- 运行成功 结果错误返回退出码
- 运行异常
那么的join需要处理异常的这种情况吗?
答案显然是不需要的 我们之前在解释健壮性的时候已经说过了 如果一个线程出现异常情况 那么操作系统就会发信号给进程从而杀死进程 那么这个时候我们进程接受异常也就没有意义了
Tip: 这里还需要注意的一点是如果我们创建了多个线程 我们等待只能使用for循环一个个等待
线程终止
我们在上面的线程等待代码中已经学习了第一个线程终止的方案 函数中return
下面是return的两种情况
- main函数return代表主线程和进程退出
- 其他线程函数return只代表线程return
pthread_exit
我们除了使用return之外还可以使用pthread_exit函数来终止一个线程 它的函数原型如下
void pthread_exit(void *retval);
返回值说明:
我们这里退出的返回值是void 这是很合理的
因为一旦退出之后这个线程就不存在了 接受返回值也根本没有意义
参数说明:
- retval:线程退出时的退出码信息
下面是代码演示
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
W> 6 void* run_thread(void* args)
7 {
8 printf("im new thread my tid is:%lu\n",pthread_self());
9 sleep(3);
10 pthread_exit((void*)101);
11 }
12
13
14 int main()
15 {
16 pthread_t tid;
17 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
18 void* status = NULL;
19 printf("waiting ...\n");
20 pthread_join(tid ,&status);
21 printf("wait success , exit code is:%d\n", (int)(intptr_t)status);
22 return 0;
23 }
我们这里使用了thread_exit函数来进行线程退出 退出码是void* 类型的101
下面是运行结果
pthread_cancel函数
我们还可以使用cancel函数来取消一个线程 它的函数原型如下
int pthread_cancel(pthread_t thread);
返回值说明:
- 线程等待成功返回0 失败返回-1
参数说明:
- thread:被取消线程的ID
注意! 这个函数有许多种用法 我们既可以让线程取消自己 也可以让新线程取消主线程 还可以让主线程取消新线程 但是我们只建议使用主线程去取消新线程
下面是演示代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
W> 6 void* run_thread(void* args)
7 {
8 printf("im new thread my tid is:%lu\n",pthread_self());
9 while(1)
10 {
11 sleep(3);
12 }
13 }
14
15
16 int main()
17 {
18 pthread_t tid;
19 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
20 void* status = NULL;
21 printf("waiting ...\n");
22 pthread_cancel(tid);
23 pthread_join(tid ,&status);
24 printf("wait success , exit code is:%d\n", (int)(intptr_t)status);
25 return 0;
26 }
我们在创建线程之后直接取消之并且查看退出码
演示结果如下
我们可以发现退出码确实是-1
为什么退出码是-1呢?
我们可以在系统中搜索“PATHREAD_CANCELED” 这个宏定义
我们可以发现实际上它就是被定义为了 ((void*)-1)
所以说我们以后在线程中看到返回值为-1就应该明白是这个线程被取消了
线程分离
在进程控制这一章节中 我们除了讲解进程创建 等待 终止之外还讲解了一个进程替换
但是在线程中进程替换是没有意义且不被允许的
因为我们说过线程和进程是共用的同一个虚拟地址空间 它分享一部分数据和代码 如果我们修改了这些代码和数据那么整个虚拟地址空间都会受到影响 所以说线程替换是不被允许的
而我们在线程控制这部分学习的是线程分离 我们可以使用pthread_detach
函数来进行线程分离 它的函数原型如下
int pthread_detach(pthread_t thread);
我们线程分离之后实际上该线程还是使用的原来的进程地址空间 如果该线程崩溃了 那么进程也会崩溃
那么我们为什么还要进行线程分离呢?
因为线程分离之后我们就不需要线程等待了 就能节约等待的时间
与此同时我们的等待函数也将会失效 不能对于这个线程进行等待操作
返回值说明:
- 线程分离成功返回0 失败返回错误码
参数说明:
- thread:被分离线程的ID
下面是代码演示
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
W> 6 void* run_thread(void* args)
7 {
8 printf("im new thread my tid is:%lu\n",pthread_self());
9 printf("hello im new thread\n");
10 sleep(3);
W> 11 }
12
13
14 int main()
15 {
16 pthread_t tid;
17 pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
18 printf("waiting ...\n");
19 pthread_detach(tid);
20 sleep(3);
21 return 0;}
我们在创建完这个线程之后就分离它 并在三秒之后终止主线程和分离的线程
演示结果如下
线程id和进程地址空间布局
我们可以通过下面的代码来查看主线程的线程id
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
6
7 int main()
8 {
9 while(1)
10 {
W> 11 printf("main thread id is:%x\n",pthread_self());
12 sleep(1);
13 }
14 return 0;
15 }
演示结果如下
此外我们还可以创建一个新线程来打印它的tid
代码如下
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
W> 6 void* thread_run(void* args)
7 {
8 while(1)
9 {
W> 10 printf("im new thread my tid is:%x\n", pthread_self());
11 sleep(1);
12 }
13 }
14
15 int main()
16 {
17 pthread_t tid;
18 pthread_create(&tid , NULL ,thread_run,(void *) "hello world" );
19 while(1)
20 {
W> 21 printf("main thread id is:%x\n",pthread_self());
22 sleep(1);
23 }
24 return 0;
25 }
~
演示结果如下
我们这里直接交代结论:
我们这里查看所有线程id它都是一个内存中的虚拟地址
我们通过ldd指令可以查看到
我们连接的线程库实际上就是一个动态库 它既然是个动态库 那么它肯定就是一个文件
当这个文件被加载到物理内存之后会经过页表的映射加载到进程地址空间中的共享区去
我们说每个进程都有自己栈空间 实际上这个栈空间和我们所理解的栈空间是不一样的
线程采用的栈就是在共享区中开辟的 除此之外线程还有自己的struct pthread 当中包含了对应线程的各种属性
每个线程还有自己的线程局部存储 当中包含了对应线程被切换时的上下文数据
我们每增加一个线程在共享区中就会增加一个这样子的结构体
上面我们所用的各种线程函数 本质都是在库内部对线程属性进行的各种操作 最后将要执行的代码交给对应的内核级LWP去执行就行了 也就是说线程数据的管理本质是在共享区的
而LWP就存储在线程的struct pthread中
所以说我们看到的这些tid实际上就是共享区的虚拟地址