目录
线程概念篇:
线程(轻量级进程(LWP))概念:
多线程的概念:
标识:pid和tgid的区分
进程与线程的区别:
多线程和多进程的区别:
线程函数篇:
线程创建:
线程终止(线程退出):
线程等待:
线程分离:
主线程退出的状况
线程就是一个执行流,创建一个线程就相当于在内核当中创建一个PCB(task_struct),创建出来的PCB当中的内存指针是指向进程的虚拟地址空间的。
但我们存在一个疑问,创建子进程我们知道一个函数vfork,子进程拷贝父进程的PCB,这样子进程可执行父进程的进程虚拟地址空间,这样就存在一个大的问题:调用栈混乱;这也是我们不常用vfork函数的原因,倘若用vfork创建子进程的时候,我们必须让父进程进行进程等待,以避免产生调用栈混乱的问题,这样做和单进程效率一样,且浪费资源。
而我们如果创建一个线程,同样会出现刚才的情况,线程的PCB的内存指针指向进程的虚拟地址空间,这会不会带来vfork函数相同的问题?调用栈混乱
我们创建多线程的时候,其实并不会和vfork创建多进程一样,出现调用栈混乱的问题!
原因:我们在创建多线程的时候,创建一个线程就等于在内核中创建了PCB,而PCB的内存指针指向进程的虚拟地址空间,如果仅是如此,那肯定会造成调用栈混乱的问题!但是创建线程后,会在共享区开辟一段空间,保存着该线程自己独有的东西,其中就含有调用栈,如此一来,就避免了调用栈混乱的问题。
那在共享区开辟的空间上都保留着那些东西呢?
这就涉及了多线程间共享和独有的问题了。
共享:进程中所有的信息对该进程内的线程都是共享的,eg:进程虚拟地址空间,文件描述符表、信号的处理方式、进程当前的工作路径、用户ID、用户组ID、程序的全局内存、堆内存等等
独有:进程内执行环境所必须的信息,eg:线程ID、栈、errno变量、信号屏蔽字、调度优先级及策略、一组寄存器、线程的私有数据等等
由于一个线程就是一个执行流,那么多个线程就是多个执行流,在多核CPU的机器当中,同一时间,每一个执行流理论上都可以拥有一个CPU,然后并行的去运行,多线程可以提高运行的效率。
线程的概念是c库当中的概念,因为线程的接口都是c库提供的。底层调用clone接口
同时,承接上面的调用栈混乱的问题:
为了避免多线程出现调用栈混乱的问题,每创建一个子线程,就会在共享区开辟一段空间,保存该线程所独有的东西
我们在PCB中即tack_struct结构体中看到两个变量:
pid_t pid;
pid_t tgid;
两个变量都是pid_t类型,他们有什么区别呢?
tgid:(thread group ID) 线程组id, 进程号,即进程内的主线程ID
pid :(process ID) 线程ID,即我们创建的子线程ID
一个进程可以包含若干线程,使用线程可以实现应用程序同时做几件事并且互相不干扰。
进程为应用程序的运行实例,是应用程序的一次动态执行,进程是操作系统进行资源分配的单位。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源。
线程与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。
多进程:每一个进程都用自己独立的虚拟地址空间,这个也是进程独立性的原因。一个进程的崩溃,不会导致另外一个进程受到影响。
多进程的程序也可以提高程序运行效率(本质也是增加执行流),但是带来了进程间通信的问题。
多线程:多个线程则共享进程的数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了利用CPU时间,同时在一个进程内运行多个任务。
但由于一个进程内多个线程共用一个虚拟地址空间,所以,一个执行流的异常会导致整个程序的退出,同时多线程还会导致程序健壮性低,代码编写复杂等问题。
优点:
缺点:
前言:线程函数都是库函数,使用线程函数的时候我们需要链接线程库即增加 -lpthread 选项
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
thread:线程标识符,帮助我们标识一个线程,其实是一个出参,还记得线程创建的时候,要在共享区开辟弊端空间保存该线程独有的信息嘛,该标识符其实是这段空间的首地址
attr:线程属性 eg:该线程的栈大小,栈的其实位置,调度优先级、分离属性等等
ulimit -s 命令可以查看栈大小
如果我们传递一个nullptr空指针,线程创建的时候将会采用默认属性
start_routine:函数指针类型,创建的线程的入口函数,即新建线程要处理的任务函数
arg:传递给线程入口函数的参数,注意:线程入口函数只能传递一个参数,如果你要传递多个参数,可以考虑自己定义一个结构体,将结构体地址作为arg进行传递
返回值:
线程创建成功,将会返回0
线程创建失败,将会返回错误码
注意arg这个参数,
因为这个参数是传递给新建线程的入口函数的参数,我们要确保这个参数可以被新创建的线程所访问到;
如果我们传递一个临时变量,一定不可以,临时变量存放在主线程的栈上,而我们新创建的线程拥有自己的栈,无法访问主线程的栈,也就无法拿取那个临时变量。
而全局变量是可以的,因为全局变量存放在数据段,数据段是进程内线程共享的;堆也是进程内线程所共享的,我们也可以动态开辟一段空间,传递给新建线程,但注意及时释放这段空间,防止内存泄漏
注意:
同进程创建一样,线程创建时,并不能保证闹个线程先被执行:新建的线程,主线程都有可能先执行
新建的线程会继承调用线程的信号屏蔽字,但是未决信号集将会被清除
线程退出的三种方法:
- return返回,退出
- 线程调用pthread_exit()函数
- 同一进程内的其他线程可以取消该线程
void pthread_exit(void *retval);
哪个线程调用这个函数,那个线程就会退出;
int pthread_cancel(pthread_t thread);
我们只需要传入一个线程标识符,就可以让该线程标识符表示的线程退出
参数:thread:想终止的线程的标识符
返回值:成功返回0;
失败返回错误码
int pthread_join(pthread_t thread, void **retval);
参数:thread:要等待的线程的线程标识符
retval:一个二级指针,出参,用于获取线程的退出状态
对应线程退出的三种方法:
- return退出;retval保存的内容是return返回的值
- pthread_exit(void*)退出;retval保存该函数的参数
- pthread_cancel(pthread_t);* retval中保存一个常数:PTHREAD_CANCELED
如果我们不关心退出状态,可以传递一个nullptr空指针
pthread_join(pthread_t,void**)是一个阻塞接口,直至等待指定的线程退出
等待接口的作用:
线程在默认创建的时候,默认属性当中被认为joinable的
joinable:当线程退出的时候,需要其他线程来回收该线程的资源,如果没有线程回收,则在共享区中,对于退出线程的空间还是保留的,退出线程的资源没有被释放,也就造成了内存泄漏
所以我们调用线程等待接口,可以使指定的线程为分离状态,释放退出线程的资源,防止内存泄漏
如果我们不想进行线程等待,可使用线程分离接口,使线程为分离状态
改变线程的joinable属性,变成detach,从而在线程退出的时候,线程在共享区开辟的资源直接被系统回收,不需要其他线程来回收该线程的资源
int pthread_detach(pthread_t thread);
参数:
thread:线程标识符
返回值:
成功返回0;
失败返回错误码
我们都知道每个进程中必定有一个主线程,倘若我们调用线程终止接口,是主线程退出,那么整个进程会退出吗?
只要有工作线程程,进程是不会退出的:(主线程类似进入Z状态,称为所谓的“僵尸线程”)
如下面代码所示:主线程退出之后,整个进程不会退出,工作线程状态在R/S之间来回切换
void* start(void* arg)
{
while(1)
{
printf("I am working\n");
}
}
int main()
{
pthread_t tid;
int ret=pthread_create(&tid,NULL,start,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
pthread_exit(NULL);
return 0;
}
如果没有工作线程,那么整个进程就会退出
void* start(void* arg)
{
// while(1)
{
sleep(1);
printf("I am working\n");
}
}
int main()
{
pthread_t tid;
int ret=pthread_create(&tid,NULL,start,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
sleep(3);
pthread_exit(NULL);
return 0;
}
注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!