线程

线程?

从进程开始延伸:
执行流:其实,多个进程就是多个执行流。一个执行流指的就是能够独立地执行一段代码和处理数据。
进程是程序运行的基本单位,进程也是资源分配的基本单位。cpu调度就是调度一个进程。
其实,线程也是一个执行流。
当我们有多个任务想要同时处理的时候,可以选择多进程,也可以选择多线程,只不过多线程所占用的资源相较于多进程要更少
那究竟什么是线程?线程是一个执行流,能够独立地执行一段代码和处理数据。
在操作系统中,进程就是pcb,pcb就是进程。
但是在Linux下,用pcb来实现线程,因此Linux下的线程实际上是一个轻量级进程(LWP)。为什么叫轻量级进程?因为轻量级进程共用大部分资源,所以相较于传统进程更加轻量化
原本pcb是用来实现进程的,但现在在Linux下,pcb是用来实现线程的。一个进程里面可以有多个线程,它们只有一份虚拟地址空间(mm_struct),也就是说它们共用很多资源。
而Linux下的进程叫做线程组。
进程是资源分配的基本单位。 一个程序运行起来后,它至少会有一个pcb,至少还得有虚拟地址空间、页表。接下来再创建一个线程,就只是创建一个pcb而已。它们会共用很多资源,而这些资源是属于整个线程组的,而不是属于某一个pcb的。
因此进程是资源分配的基本单位,而在Linux下线程才是cpu调度的基本单位,因为在Linux下pcb是线程。

线程之间资源的独有与共享:

独有:
1、栈,它们拥有同一份虚拟地址空间,但是在虚拟地址空间当中,每个线程都有自己独立的一块空间。
2、上下文,只不过在线程这通常不说它有自己独立的上下文,而是说它有自己的一套寄存器,但千万别理解成硬件上的寄存器,一个cpu就只有一套寄存器。
3、信号屏蔽字,也就是阻塞的信号集合。每个线程都可以阻塞自己不想处理的信号。
信号是针对整个进程(线程组)生效的,一个信号只需要有一个执行流来处理就行了,具体哪一个执行流/pcb/线程来处理这个信号,这是不一定的,谁占用cpu谁就处理。
4、errno,线程局部变量。
5、线程ID
6、调度优先级
共享:
1、虚拟地址空间代表全局变量(已初始化全局、未初始化全局、已初始化静态全局、未初始化静态全局,线程间通信极为方便)、代码段(如果定义一个函数,在各线程中都可以调用)、堆
2、文件描述符表
3、当前工作路径:就是你在哪个路径下执行程序
4、用户id和组id
5、信号处理方式

多任务的执行,既可以使用多进程,也可以使用多线程,分析优缺点,视场景而定。

多线程任务处理的优缺点:
1、线程间通信除了进程间的方式之外,还有更简单的就是全局数据/传参,因为可以通过虚拟地址来访问这些数据。线程间通信更加方便。
请参考1
请参考2
根据“参考2”,个人又进行验证,已初始化全局、未初始化全局、已初始化静态全局、未初始化静态全局都能线程间共享,又根据“参考1”,个人总结,已初始化\未初始化静态局部 可以通过传参(局部变量也可以通过传参)来进行线程间的共享。(栈区是不共享的,各线程私有一份,但静态局部变量却不在栈上,而且先前又提到过“数据段(存放已初始化的全局\静态全局和已初始化的局部静态变量,且初始化的值不为0)共享”,又因为已初始化全局\静态全局可以线程间共享,所以才引发“已初始化\未初始化静态局部是否涉及共享问题”的思考。)
2、创建/销毁一个线程的成本(资源成本、时间成本)相较于进程要更低。因为创建一个进程不但要创建pcb,还要创建虚拟地址空间、页表等分配各种资源,但是对于创建一个线程来说,一个pcb就搞定了,因为指向同一个虚拟地址空间,把很多数据拷贝一下就可以了,很多资源都不需要重新分配,因为共享。多线程共用进程大部分资源。
3、线程组间的调度相较于进程要更低。因为调度进程得重新加载页表信息,去映射地址。而线程组的线程共用同一份页表,根本不需要重新加载。
缺点就是线程之间缺乏访问控制,稳定性更低。
有些系统调用/异常针对的是整个进程。
1、在一个线程里面调用exit退出进程,所有线程都得退。
2、段错误,一个线程里面出现内存访问错误,整个进程都得退。
使用场景:shell用的就是多进程,害怕在完成某个功能的时候出现了异常,导致连shell都退出了。像这种对主程序稳定安全性要求更高的程序就需要使用多进程,让子进程处理任务。

线程控制

1、线程创建
2、线程终止
3、线程等待
4、线程分离
Linux下操作系统并没有提供线程的系统调用接口,因此大佬们封装了一套线程的控制接口库POSIX线程库
使用库函数实现创建线程我们称之为用户态线程,这个用户态线程在内核中使用一个轻量级进程实现调度。
创建一个用户态线程让用户控制,但是程序的调度处理都是通过轻量级进程pcb实现。

Linux下的线程其实应该是:用户态线程+轻量级进程

线程创建

#include<pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

参数1:输出型参数,用于获取线程id:线程地址空间(线程地址空间(线程在用户态的描述信息、线程的栈、线程的局部存储)在共享区)在整个虚拟空间中的首地址。
在这里插入图片描述
pthread_t pthread_self(void);//获得线程自身的操作句柄
创建一个线程,返回线程id来作为这个线程的操作句柄。
参数2:设置线程属性。通常置NULL,表示使用默认的。
参数3:线程的入口函数,void* (void *)
参数4:参数3的实参
返回值:成功0,失败的话不会设置errno。

pthread系列函数出错时不会设置全局变量errno,而是将错误号通过返回值带回。

pcb中
pid_t pid;//轻量级进程id,就是线程id。
pid_t tgid;//进程(线程组)id = 进程中主线程的pid,第一个起来的pcb,也就是运行main函数的pcb就是主线程。
而ps在查看的时候,它只查看一个进程(线程组)的首线程(tgid)信息,它并不会列出所有。

在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程在内核中都对应一个调度实体,也都拥有自己的进程描述符(task_struct结构体)。 在没有线程之前,一个进程对应内核中的一个进程描述符,也就对应一个进程id(pid)。但是引入线程概念之后,情况发生了变化,一个进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核中都有一个自己的进程描述符,进程和内核中的描述符一下子就变成了1 : N的关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程id,如何解决上述问题呢? Linux内核引入了线程组的概念。
多线程的进程,又被称为线程组,线程组内的每一个线程在内核中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程id,其实不然,它对应的是线程id;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程id,也就是主线程id。

线程中id的讨论
pcb中的pid:轻量级进程id,就是线程id,唯一标识,这属于线程调度的范畴。而pthread_create的参数1也是线程id,但是两者是截然不同的,pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程id(线程地址空间首地址,方便用户操作线程。),属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程id来操作线程的。
Linux提供了gettid系统调用来返回其线程id,可是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用,如果确实需要获得线程id,可以采用如下方法:

#include<sys/syscall.h> 
pid_t tid; 
tid = syscall(SYS_gettid);

pcb中的tgid:进程(线程组)id,默认等于首线程的pid。
ps -efL | head -n 1 && ps -efL | grep a.out
LWP:轻量级进程(线程)
NLWP:当前进程(线程组)中有几个轻量级进程(线程)
在这里插入图片描述

线程终止

1、return,
注意:不能在main函数中return,因为此时是退出了进程,导致所有线程退出。
而从 线程函数 return,这种方法只是对当前线程进行终止,并不会对整个进程有影响。
2、exit退出的也是进程。而且在非主线程中调用exit也会退出整个进程。
3、void pthread_exit(void *retval);//退出线程自身,谁调用谁退出。参数是线程的退出返回值。注意,无返回值,线程结束的时候无法返回到它的调用者(自身),同进程程序替换。
注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。
4、int pthread_cancel(pthread_t thread);//取消线程,让指定线程退出。
一定要查一下相关博客进一步了解此函数。
违规操作:
1、还可以通过该函数把主线程退出,主线程退出后并不影响其他线程的运行。
僵尸线程通常体现不出来,exit.c和exit2.c和exit3.c普通线程退出,根本就不体现。但是主线程退出,就会体现出来。主线程退了,但是ps -aux | grep exit4这个首线程的信息还得在。但是只要kill -9 +任意线程id就可以了,因为信号是对整个进程生效。
代码
虽然线程没有提到有僵尸线程这一说法,但是线程退出之后,它依然无法自动释放资源,会造成资源泄漏。
一个线程退出之后,为了保存自己的退出返回值,保存在自己的线程地址空间当中,因此一个线程退出之后,在没有进行获取返回值之前,操作系统不会自动回收这块资源,需要用线程等待来获取返回值,释放资源。
违规操作2、自己取消自己

线程等待

等待指定线程退出,获取退出线程的返回结果,释放退出线程资源,就是线程地址空间。
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。并且创建新的线程不会复用刚才退出线程的地址空间
线程等待的前提:一个线程创建出来,默认有一个属性叫joinable,处于joinable属性的线程退出后,不会自动释放资源,需要被其他线程等待,才能释放资源。处于joinable属性的线程必须被等待,否则造成资源泄漏。一个线程也只有处于joinable状态的时候,才需要被等待,其他属性是不需要的。
int pthread_join(pthread_t thread, void **retval);//阻塞并且只能等待指定线程退出。通过参数2获取返回值。调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1、如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2、如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。
3、如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4、如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

线程分离

是将线程的joinable默认属性(新创建的线程是joinable的)修改为detach属性。可以是线程组内其它线程对目标线程进行分离,也可以是线程对自己进行分离。
线程若处于detach属性,则线程退出后将自动被回收资源,这个线程也就不需要被主线程等待了,因为等待是毫无意义的,线程退出,返回值所占用的空间已经被回收了。

int pthread_detach(pthread_t thread);//成功返回0;失败返回错误码。
pthread_detach(pthread_self());//线程对自己进行分离

线程分离的适用场景:对线程的返回值不关心。
默认情况下,新创建的线程是joinable属性的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成资源泄漏。如果不关心线程的返回值,pthread_join便是一种负担,这时,我们可以告诉操作系统,当线程退出时,释放其对应资源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值