目录
Linux线程概念
什么是线程
二级页表
线程的优点
线程的缺点
线程异常
线程用途
Linux进程和线程
线程共享资源
线程和进程的关系图
Linux线程控制
POSIX线程库
线程创建
线程等待
线程终止
线程分离
Linux线程概念
什么是线程
- 在一个程序里的一个执行流叫做线程(thread),线程是“一个进程内部的控制序列”。
- 一切进程都至少有一个线程
- 线程在进程内部运行,本质是在进程地址空间中运行。
- 在Linux系统中,CPU眼中,看到的PCB都要比传统的更轻量
- 通过进程地址虚拟空间,可以看到进程的资源,这样通过虚拟地址就可以把每个进程资源合理的分给每个线程,形成线程执行流
我们知道一个进程(PCB)都有进程控制块(task_struct),进程地址空间(mm_struct)以及页表的建立,虚拟地址和物理地址就是通过页表进行映射的。
如果我们在创建一个“进程”时,只创建一个task_struct(用于控制这个进程),然后使创建出来的父进程和子进程都指向同一块进程地址空间。结果如下图:
这种指向同一个进程地址空间的task_struct就是线程所以上面实际就是4个线程:
- 每一个线程都是一个执行流,是进程的一个分支。
- 可以看到每一个线程都指向同一个进程地址空间,那么这个进程申请过的资源,在每一个线程中都可见
重新理解进程
下面红色框框的内容就是进程。
所以,进程并不是通过task_struct衡量的,除了task_struct之外,一个进程还要有进程地址空间,文件,信号等等,合起来才是一个进程。
如果我们站在内核角度理解线程:承担分配系统资源的基本实体,叫做进程。
之前我们所了解的进程是单执行流的,现在有了线程后,进程可以是多执行流的。
在Linux中,站在CPU的角度 ,能否识别task_struct是进程还是线程?
不能!也不需要,因为CPU只关心一个一个的独立执行流,控制块(task_struct),无论进程内部只有一个执行流还是多个执行流,CPU都是以task_struct为单位进行调度的。
如果是单执行流在只调用这个执行流,如果是多执行流就是轮换地调用。
Linux下并不存在真正的多线程,都是用进程模拟的
由上面可知,我们操作系统中的线程数量一定是大于等于进程的。
如果要支持线程,就得对这些线程进行管理,增删查改回收,这一套还得重新设计模块。
我们知道如果要设计线程这一套实际是和进程差不多的,所以Linux中直接复用了进程控制块(task_struct),所以线程都是轻量级的进程。
不过在WINDOWS系统中,支持了真的线程。
既然没有真正线程的概念,那么就没有真正意义上的线程的调用接口调用
顾名思义:所以我们系统只有一个轻量级进程创建的接口:
pid_t vfork(void);
创建子进程并且和父进程共用一个进程地址空间。
返回值和fork函数相同:父进程返回子进程 进程ID,子进程返回0;
下面代码使用了一个全局变量,此时查看这个全局变量在父进程和子进程中的打印情况。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>int global_val = 704;
int main(){//创建轻量级进程pid_t pid = vfork();if(pid == 0){//子进程global_val += 1118;printf("child pid:%d,ppid:%d,global_val:%d\n",getpid(),getppid(),global_val);exit(0);}//父进程sleep(3);printf("father pid:%d,ppid:%d,global_val:%d\n",getpid(),getppid(),global_val);return 0;
}
如上图所示:我们的子进程和父进程看到了同一个全局变量,所以他们共用了一个空间。
原生线程库pthread
上面说Linux系统中,不知道线程只知道轻量级进程,但是站在我们用户的视角上,我们是想创建线程而不是进程,所以更想用thread相关的接口,所以系统为用户层提供了原生线程库pthread。
原生线程库就是对轻量级进程的系统调用进行了封装,给用户使用
所以在Linux下学习线程直接使用线程库即可,不是系统调用
二级页表
32位平台为例,32位平台的地址有2的32次方,所以我们就有2的32次方的地址需要映射
如果页表如下图:
那这张页表就需要2的32次方个表项,每个表项除了存着物理地址外还有其他信息,比如区分是用户级页表还是内核级页表。
那这样一个存储一个物理地址和一个虚拟地址需要8字节,假设总共一个表现要10字节。
2的32次方 * 10 是40gb,32位系统的内存都只有4gb,所以这样设计是不合理的
真正的页表
映射过程如下:
1.选择虚拟地址的前十个比特位去目录中找,找到对应的页表(就像每本书前面都是目录)
2.向后继续选择10个比特位此时就是去对应的页表中找到物理内存对应页框的起始地址(二级目录)。
3.最后剩下的12个比特位,是偏移量,从我们找到的其实位置开始,我们需要的内容离起始位置的距离。
解释:
1.物理内存实际就是被分成了4kb一个的小块,磁盘上的程序也是被分成4kb一个的页帧的,所以内存和磁盘交互以4kb进行加载和保存的。
2.4kb就是2的12次方字节,一个页框有2的12字节,我们访问内存是以字节为单位的,所以一个页框就是2的12次方个地址,所以剩下的12个比特位就足以表示这些地址。偏移量
如下图:
所以其实我们之前的页表是有两张的,第一个是页目录第二个页表。
每一个表现依然算10字节,页目录和页表都是2的10次方个,所以2的10次方 * 2的10次方 * 10最终就等于 10MB,这样和之前相比小了太多了。
上述的过程都是在CPU中的一个硬件执行的MMU,一种硬件映射,而页表是软件映射,所以虚拟地址和物理地址的转换是软硬件结合的方式
注意:32位下只用了两张页表,64位需要的是更多级的页表了。
修改常量字符引起的段错误
和这张图描述的一样,在我们虚拟地址映射物理地址时,常量字符的RWX权限是只读的,所以对其修改就触发了MMU内部硬件错误,操作系统就会去识别是哪个进程的错误将其停止。
线程的优点
1.创建一个线程的代价比创建一个新进程小的多。
2.与进程之间切换相比,线程之间转换需要操作系统做的工作较少。
3.线程占用的资源比进程少得多。
4.能充分利用多处理器的可并行数量
5.等待慢速IO时,程序也可以执行其他的任务
6.计算密集型应用,为了能在多处理器系统上允许,就可以把一个任务分成多个线程中实现。
7.IO密集型,将IO操作重叠,线程可以同时等待不同的IO操作
线程的缺点
- 健壮性降低:线程因为可能是同一个进程空间,所以在同一时间内,可能一些不被共享的变量被另一个线程修改,造成影响,所以说线程是没有保护的
- 缺乏访问控制:进程是访问控制的单位,在一个线程如果调用有关进程的系统调用接口会造成影响
- 编程难度高:编写和调试一个多线程程序比单线程程序难的多
线程异常
单个线程只要出现了除零,野指针等问题都会导致整个程序崩溃。因为线程其实就是执行分支,都是一个进程,如果有一部分代码出现了这些错误,触发信号,终止进程,所有线程退出。
线程用途
合理使用多线程,提高CPU密集型程序执行效率
合理使用多线程,提高IO密集型的用户体验(一边下载东西,还可以做其他的事情)。
Linux进程和线程
进程是分配资源的基本实体,线程是调度的基本单位
线程共享进程的数据,同时也需要有自己的数据
- 线程ID。
- 一组寄存器。(存储每个线程的上下文信息)
- 栈。(每个线程都有临时的数据,需要压栈出栈)
- errno。(C语言提供的全局变量,每个线程都有自己的)
- 信号屏蔽字。
- 调度优先级。
线程共享资源
因为在同一个进程地址空间,所以代码段,数据段,都是共享的:
定义的函数是共享的,全局变量是共享的。
除了函数和变量外,下列进程资源也是共享的:
- 文件描述符表。(进程打开一个文件后,其他线程也可以使用和看见这个文件)
- 信号处理方式。(SIG_IGN,SIG_DFL或者自定义的信号函数)
- 当前的工作目录。(cwd)
- 用户ID和组ID
线程和进程的关系图
其实就和上面说的一样,一个进程里有一个或者多个线程。
Linux线程控制
POSIX线程库
pthread线程库是应用层的原生线程库:
1.应用层就说明这个pthread库不是系统自带的,而是第三方提供。
2.原生指的是大部分Linux系统都是默认带上该线程库。
3.与线程有关的函数是一个完整的系列,绝大多数的函数以"pthread_"命名。
4.头文件就是<pthread.h>
5.这些外来的库是需要链接,所以在编译时需要加上-l pthread指令。由于上面说了是原生的,所以不需要用 -L -I 指定指定位置的库和头文件库了。
错误检查:
- 传统的函数,成功返回0,失败返回-1,并且要设置全局变量errno以表示错误。
- pthread函数出错时不设置全局变量errno,而是将错误代码通过返回值返回。
- pthread里也提供了线程内的errno变量,支持其他需要使用errno的代码,但是对于pthread函数的错误,建议通过返回值判定,因为读取返回值的消耗,小于读取errno的消耗。
线程创建
函数如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数说明:
thread:获取创建成功的线程ID,是一个输入输出型参数。
attr:设置创建线程的属性,传入NULL表示使用默认属性。
start_routine:这是一个函数的地址,表示线程启动后需要执行的函数。
arg,传递给线程函数的参数。
返回值说明:
线程创建成功返回0,失败返回错误码。
主线程创建一个新线程
主线程中通常得有关闭操作。
下面代码我们使用pthread_create创建一个新线程,执行自己的代码:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;while(1){printf("i am %s\n",msg);sleep(1);}}
int main(){//创建线程pthread_t tid;pthread_create(&tid,NULL,Routine,(void*)"thread 1");//主线程死循环打印while(1){printf("i am main thread\n");sleep(2);}return 0;
}
结果如图所示:我们2秒打印main,1秒打印pthread 1.
ps -aL是专门用来显示当前的轻量级进程的。-L选项
上面的TWP其实就是线程ID,上述的两个线程的PID相同,所以就是同一个进程。
下述代码里是证明二者是同一个进程:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;while(1){printf("i am %s pid:%d,ppid:%d\n",msg,getpid(),getppid());sleep(1);}}
int main(){//创建线程pthread_t tid;pthread_create(&tid,NULL,Routine,(void*)"thread 1");//主线程死循环打印while(1){printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());sleep(2);}return 0;
}
pid 和 ppid 是相同的说明主线程和新线程就是一个进程。
主线程创建一批新进程
下面代码我们创建5个进程,并且都执行Rountine,说明这个函数是会被重入的(一时间多个线程使用):
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;while(1){printf("i am %s pid:%d,ppid:%d\n",msg,getpid(),getppid());sleep(1);}}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);}//主线程死循环打印while(1){printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());sleep(2);}return 0;
}
我们可以看到结果很混乱,但是每一个线程都是出来的了,且是同一个进程的。
并且通过 ps -aL也能看到5个线程已经创建,外加上一个主线程。
线程ID
常见两种线程ID获取方式:
1.创建线程时的第一个参数。
2.通过pthread_self函数获得。(和getpid类似)
函数原型如下:
pthread_t pthread_self(void);
下面代码多了两种线程id的打印:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;while(1){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());sleep(1);}}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程死循环打印while(1){printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());sleep(2);}return 0;
}
我们发现两种方法获取的线程ID是相同的。
注意:使用pthread_self获取的线程ID和LWP的值不同,一个是用户级的线程ID,一个是内核的轻量级进程的ID,但他们是一对一的关系
线程等待
线程也是需要等待的,和进程一样,不然会出现僵尸进程问题,主线程要负责新线程的等待,不然这个资源不会被回收。内存泄漏
线程等待函数如下:
int pthread_join(pthread_t thread, void **retval);
参数说明:
thread:线程ID 用户级的
retval:线程退出时的退出码信息
返回值说明:
线程等待成功返回0,失败返回错误码。
调用线程等待函数的线程会被挂起,直到对应的线程被回收。
总结:
1.如果thread是通过return返回,则会返回这个值。
2.如果线程是别的线程使用pthread_cancel异常终止掉,retval指向常数PTHREAD_CANCELED.
3.如果线程是自己调用pthread_exit终止,retval指向的就是传给pthread_exit的参数
4.如果不需要返回值,给retval设置NULL即可
PTHREAD_CANCELED实际就是-1.
下列代码我们不关心退出信息,等待线程结束:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 5;while(count--){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());sleep(1);}return NULL;
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());for(int i = 0;i<5;++i){pthread_join(tid[i],NULL);printf("%d[%ld] thread quit ...\n",i,tid[i]);}return 0;
}
结果如图所示5秒后主线程回收等待了所有线程。
下面代码我们用返回值返回了结果:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 5;while(count--){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());sleep(1);}return (void*)2025;
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());for(int i = 0;i<5;++i){void* ret = NULL;//第二个参数是二级指针,可以把ret当做输入输出参数pthread_join(tid[i],&ret);printf("%d[%ld] thread quit ...,quit code:%d\n",i,tid[i],(int)ret);}return 0;
}
pthread_join是以阻塞的方式等待线程的,所以要放到准确的位置。
并且线程只需要获得正常退出的码即可,因为进程只要出错就会把整个程序都关闭,所以线程只要返回结果正常退出的结果即可。其他的错误线程就可以报出。
线程终止
如果只是终止某个线程而不是整个进程,三种方法:
1.return线程
2.线程自己调用pthread_exit()函数退出
3.其他线程可以通过pthread_cancel()终止同进程的其他线程。
return
在main函数中使用就等于退出整个进程,进程都没了其他线程相对的就退出释放了。
下面代码:主线程直接退出:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;while(1){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());sleep(1);}return (void*)2025;
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());return 0;
}
结果如图,我们新线程都只打印了一遍,因为主线程return了进程就结束了。
pthread_exit函数
void pthread_exit(void *retval);
参数说明:
retval:线程退出时的退出码信息
注意:给retval这个指针的内容不能是线程栈上分配的,其他线程得到这个返回指针时,线程已经销毁退出了
下面代码我们设置void*给到主线程join参数:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 0;while(count < 3){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());count++;sleep(1);}pthread_exit((void*)2025);
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());//主线程获取线程退出信息for(int i = 0;i<5;++i){void *ret = NULL;pthread_join(tid[i],&ret);printf("thread %d[%lu]...quit,exitcode:%d\n",i,tid[i],(int)ret);}return 0;
}
如图我们的线程最后都成功退出且返回了退出码。
pthread_cancel函数
这个函数用于线程的取消。
int pthread_cancel(pthread_t thread);
参数:thread:就是某个线程在用户层面使用的线程ID
返回值说明:
成功返回0,错误返回错误码
下面代码我们让线程取消自己,然后退出码就为-1:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 0;while(count < 3){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());count++;sleep(1);pthread_cancel(pthread_self());}pthread_exit((void*)2025);
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());//主线程获取线程退出信息for(int i = 0;i<5;++i){void *ret = NULL;pthread_join(tid[i],&ret);printf("thread %d[%lu]...quit,exitcode:%d\n",i,tid[i],(int)ret);}return 0;
}
结果如图所示:退出码最终为-1.
但是一般都不是让线程取消自己,而是让别的线程来取消这个线程,代码如下主线程取消:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 0;while(count < 3){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());count++;sleep(1);}pthread_exit((void*)2025);
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束//取消4个线程pthread_cancel(tid[0]);pthread_cancel(tid[1]);pthread_cancel(tid[2]);pthread_cancel(tid[3]);printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());//主线程获取线程退出信息for(int i = 0;i<5;++i){void *ret = NULL;pthread_join(tid[i],&ret);printf("thread %d[%lu]...quit,exitcode:%d\n",i,tid[i],(int)ret);}return 0;
}
结果如图所示:我们发现前4个线程被取消了退出码为-1,而thread 4正常退出,退出码为我们设定的2025.
同时我们也可以让其他线程来取消主线程,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
pthread_t main_tid;
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换char* msg = (char*)arg;int count = 0;while(count < 3){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());count++;sleep(1);pthread_cancel(main_tid);}pthread_exit((void*)2025);
}
int main(){//创建线程main_tid = pthread_self();pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}//主线程等待线程结束//取消4个线程// pthread_cancel(tid[0]);// pthread_cancel(tid[1]);// pthread_cancel(tid[2]);// pthread_cancel(tid[3]);printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());//主线程获取线程退出信息for(int i = 0;i<5;++i){void *ret = NULL;pthread_join(tid[i],&ret);printf("thread %d[%lu]...quit,exitcode:%d\n",i,tid[i],(int)ret);}return 0;
}
不过这样执行的话我们就看不到主线程后面的执行了,并且线程也没被等待。
线程分离
和join是冲突的,因为join说明我们要等待线程退出,而线程退出则是我们不想关心线程的退出信息,让其退出后自行结束就好,并且可以让别的线程来分离自己和主线程,也可以自己分离自己。因为等待join是阻塞的所以不想等待就是分离
pthread_detach线程分离函数
int pthread_detach(pthread_t thread);
参数:线程ID
返回值:成功返回0,失败返回错误码。
下面代码我们让线程自己分离自己,此时主线程就不会收到退出信息
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void* Routine(void*arg){//线程执行的函数,必须是void*和void*的参数对应pthread_creat,在上面可以转换pthread_detach(pthread_self());char* msg = (char*)arg;int count = 0;while(count < 3){printf("i am %s pid:%d,ppid:%d,tid:%ld\n",msg,getpid(),getppid(),pthread_self());count++;sleep(1);}pthread_exit((void*)2025);
}
int main(){//创建线程pthread_t tid[5];for(int i = 0;i<5;i++){//为了传递的参数是不一样的,我们采用sprintf指定格式char* buffer = (char*)malloc(64);sprintf(buffer,"thread %d",i);pthread_create(&tid[i],NULL,Routine,(void*)buffer);printf("%s tid is %ld\n",buffer,tid[i]);}// pthread_cancel(tid[3]);while(1){printf("i am main thread,pid:%d,ppid:%d,tid:%ld\n",getpid(),getppid(),pthread_self());sleep(1);}return 0;
}
结果如图:我们的其他线程退出后,只有主线程还在打印。 且线程也只剩下主线程了。
pthread_t是什么?怎么来的?
首先我们知道线程库并不是系统自带库,而是一个动态库。
由图可以看出,因为动态库是加载到共享区的,所以所有线程都可看见。
然后我们要知道每一个线程都有一个struct pthread 和 进程间区分一样。每个进程都有自己的私有的栈,主线程的栈用的就是进程地址空间的栈,而其他线程的栈则是开辟在共享区中的。
如上图我们可以看出动态库里又像我们进程分配一样,给线程分配了自己的空间。所以只要找到线程的起始地址就可以找到线程的信息。
pthread_t,LInux实现的NPTL线程库看,线程ID的本质就是虚拟地址,因为进程中的虚拟地址都是不同的,所以可以用来分配给线程ID。
下面代码我们以地址形式打印线程ID:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* Routine(void* arg)
{while (1){printf("new thread tid: %p\n", pthread_self());sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, NULL, Routine, NULL);while (1){printf("main thread tid: %p\n", pthread_self());sleep(2);}return 0;
}
发现结果就是一个地址