在《程序员的自我修养》这本书中,对Linux下的多线程做了这样的描述:
Windows对进程和线程的实现如同教科书一样标准,Windows内核有明确的线程和进程的概念。在Windows API可以使用明确的API:CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操纵它们。但对于Linux来说,线程并不是一个通用的概念。
Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程的概念。
Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程
可以给出以下结论:
其本质就是再在当前进程组中创建一个task_struct结构体,它拥有着和主线程不同的pid,指向同一块虚拟进程地址空间。
独有:
在进程虚拟地址空间的共享区当中,调用栈,寄存器, 线程ID,errno,信号屏蔽字, 调度优先级独有
寄存器独享:
当操作系统调度进程的时候一定是以task_struct结构体调度,而task _struc结构体是以双向链表存储,而操作系统调度时是从就绪队列中调度已经就绪的进程,在这里也就是轻量级进程-线程,当调度时一定会有其他线程被切出,而它切出时寄存器中存储的就是当前要执行的指令,所以要用结构体中上下文信息保存
线程ID独享:每个线程就是一个轻量级进程,所以它有自己的pid
errno独享:当线程在执行出错时会返回一个errno,这个errno属于当前自己的线程错误
信号屏蔽字独享:阻塞位图
调度优先级独享:每个进程/线程在执行时被调度的优先顺序
共享:
共享:文件描述符表,用户id,用户组id,信号处理方式,当前进程的工作目录
优先:
缺点:
函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* mythread_start(void* arg){
6 printf("I am work thread\n");
W> 7 }
8
9 int main(){
10 pthread_t tid;
11 int ret = pthread_create(&tid, NULL, mythread_start, NULL);
12 if(ret < 0){
13 perror("pthread_create");
14 return 0;
15 }
16 return 0;
17 }
注意在makefile文件中链接线程库
执行结果:
很遗憾我们没看到应该存在的输出,这是什么原因呢?
因为线程都是需要操作系统进行调度的,我们在main函数中创建出来一个线程,但是我们的线程还没被调度,main线程就执行结束返回了,main函数返回就意味着进程结束了,进程结束了我们的线程就不存在了,自然不会再给出任何打印。
那我们想看到现象要怎么做呢?很容易想到,让main函数晚一点退出,给工作线程充足的时间能被操作系统调度,我们让main函数在返回前执行sleep(2);
再执行:可以看到工作线程执行了它的代码
为了观察一下线程堆栈和函数堆栈,我们索性让main函数和线程入口函数都进入睡眠状态,修改后代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* mythread_start(void* arg){
6 while(1){
7 sleep(1);
8 printf("I am work thread\n");
9 }
10 }
11
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, mythread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 while(1){
20 sleep(1);
21 }
22 return 0;
23 }
我们看看此时的调用堆栈:
我们试着给线程传递一下局部变量,代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 void* mythread_start(void* arg){
6 int* i = (int*)arg;
7 printf("I am work thread %d\n", *i);
W> 8 }
9
10 int main(){
11 pthread_t tid;
12 for(int i=0; i<5; ++i){
13 int ret = pthread_create(&tid, NULL, mythread_start, (void*)&i);
14 if(ret < 0){
15 perror("pthread_create");
16 return 0;
17 }
18 }
19 sleep(1);
20 return 0;
21 }
观察一下程序执行结果:
我们的预期是打印0-4的数字,但是执行几次发现,首先每次执行结果并不一样,其次并不按照我们预期的结果进行打印,这是怎么回事呢?是因为线程是抢占式执行的,可能是我们将所有的线程创建出来,再去执行线程的代码,或者说一边创建一边执行代码, 线程的执行顺序不确定,某个线程访问数据的时间也不确定,导致了我们上述那么多种执行结果,还有一种结果是访问数据5,i是我们for循环种的局部变量,如果for循环退出后还有线程去访问i,这是十分危险的,因为i已经被释放了,此时再对它进行访问,就有可能导致错误。
解决上面的方式有两种一种是在main函数中创建一个变量,只要main函数存在,其他那个变量就存在。而main函数退出线程也就退出了,不存在非法访问。这种是解决非法访问的问题。
另一种方式是在堆上申请空间,这样保证每个进程访问的数据是自己对应的数据
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4 #include<stdlib.h>
5
6 void* mythread_start(void* arg){
7 int* p = (int*)arg;
8 printf("I am work thread%d\n", *p);
9 free(p);
W> 10 }
11
12 int main(){
13 pthread_t tid;
14 for(int i=0; i<5; i++){
15 int *p = (int*)malloc(sizeof(int));
16 *p = i;
17 int ret = pthread_create(&tid, NULL, mythread_start, (void*)p);
18 if(ret < 0){
19 perror("pthread_create");
20 return 0;
21 }
22 }
23 sleep(1);
24 return 0;
25 }
执行结果:
总结:
线程终止的方法:
1、从线程入口函数种return返回
2、pthread_exit(void* retval)函数,retval:线程退出时, 传递给等待线程的退出信息;作用:
谁调用谁退出,主线程调用主线程终止,工作线程调用工作线程终止
3、pthread_cancel(pthread_t)参数是一个线程标识符,想要取消哪个线程,就传递哪个线程的标识符
补充一个函数:pthread_t pthread_self(void):返回调用此函数的线程id
代码演示:
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 printf("I am work pthread\n");
7 while(1){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 pthread_cancel(tid);
20 while(1){
21 sleep(1);
22 printf("I am main pthread\n");
23 }
24 return 0;
25 }
执行结果:
能看到线程并没有立即终止,而是执行了一下线程种的命令然后才终止
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 printf("I am work pthread\n");
7 while(1){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 getchar();//设置阻塞,当接受到字符后主线程将结束
20 pthread_cancel(pthread_self());//结束主线程
21 while(1){
22 sleep(1);
23 printf("I am main pthread\n");
24 }
25 return 0;
26 }
设置阻塞的目的是为了查看进程id,以观察进程
执行结果:
getchar之前的状态:
可以得出结论:主线程先退出,工作线程没退出,主线程变成僵尸进程
代码思路:将while循环去掉,让线程退出的下一句代码是return 0,观察程序状况
代码如下:
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 printf("I am work pthread\n");
7 while(1){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 getchar();//设置阻塞,当接受到字符后主线程将结束
20 pthread_cancel(pthread_self());//结束主线程
21 //while(1){
22 // sleep(1);
23 // printf("I am main pthread\n");
24 //}
25 return 0;
26 }
执行结果:
可以发现这次进程直接退出了,主线程也不是僵尸状态了,这时因为当我们执行pthread_cancle函数时,结束一个线程时,他会执行下一行命令,这时我们将主线程退出了,它在退出前执行了return 0,就会使得整个进程结束,那么此时工作线程也就退出了
代码思路:让主线程退出,然后工作线程等待10s之后退出
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 int count = 30;
7 while(count--){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
W> 11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 //getchar();//设置阻塞,当接受到字符后主线程将结束
20 pthread_cancel(pthread_self());//结束主线程
21 while(1){
22 sleep(1);
23 printf("I am main pthread\n");
24 }
25 return 0;
26 }
执行结果分析:
当主线程退出而工作线程不退出时,我们是无法看到进程的调用栈信息的
线程在创建出来的时候,属性默认是joinable属性,意味着线程在退出的时候需要其他执行流(线程)来回收线程的资源(主要是退出线程使用到的共享区当中的空间)
接口:
int pthread_join(pthread_t thread, void **retval);
功能:若线程A调用了该函数等待B线程,A线程会阻塞,直到B线程退出后,A线程才会解除阻塞状态
参数:
线程退出方式 | *retval保存的东西 |
---|---|
return | 入口函数返回值 |
pthread_exit函数 | pthread_exit函数参数 |
pthread_cancel函数 | PTHREAD_CANCEL宏定义 |
返回值:成功返回0,失败返回错误码
代码思路:让工作线程等待30s退出,然后在主线程中等待工作线程退出
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 void* pthread_start(void *arg){
6 int count = 30;
7 while(count--){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 pthread_join(tid, NULL);
20 return 0;
21 }
执行分析:
分离线程是将线程标记成已分离,其属性从joinable变成detach,对于detach属性的线程终止后,系统会自动回收其资源,不用任何线程回收其资源
接口:
int pthread_detach(pthread_t thread);
功能:将线程标记为已分离,目的是当分离的线程终止时,其资源会自动释放,防止产生僵尸进程,防止内存泄漏
参数pthread_t:需要标记分离的线程标识符
调用pthread_detach函数的位置可以是:
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 int count = 10;
7 while(count--){
8 sleep(1);
9 printf("I am work thread-l\n");
10 }
W> 11 }
12 int main(){
13 pthread_t tid;
14 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 //pthread_join(tid, NULL);
20 while(1){
21 sleep(1);
22 }
23 return 0;
24 }
执行结果分析:
可以看到它运行完直接退出了,也没有变成僵尸状态
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
W> 5 void* pthread_start(void *arg){
6 pthread_detach(pthread_self());
7 int count = 30;
8 while(count--){
9 sleep(1);
10 printf("I am work thread-l\n");
11 }
W> 12 }
13 int main(){
14 pthread_t tid;
15 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
16 if(ret < 0){
17 perror("pthread_create");
18 return 0;
19 }
20 //pthread_join(tid, NULL);
21 while(1){
22 sleep(1);
23 }
24 return 0;
25 }
执行分析:
结论:无论其他线程等待不等待工作线程退出,回收它的退出状态信息,工作线程都不会变为僵尸状态。