线程
一、线程基本概念
首先要说的就是在我们Linux下并不存在真正的线程,在Linux下,线程采用进程模拟实现的。当我们在单个进程中需要处理多个任务时,又不能创建多个进程,这时我们就引入了线程的概念。在Linux下,由于线程是用进程实现的,所以我们也罢线程叫做 轻量级进程 (LWP:light weight process),它的本质仍是进程。进程其实就是一个线程组,其中包含一个或者多个线程。
二、Linux下内核线程的实现原理
在类Unix系统中,早期是没有线程的概念的,在80年代左右才引进这个概念,它是利用进程实现线程的概念的。所以进程与线程是密切相关的。
- 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
- 进程可以蜕变成线程
- 线程可看做寄存器和栈的集合
- 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
注:三级映射:进程PCB->页目录->页表->物理页面->内存单元
对于进程来说,相同的一块虚拟地址在不同的进程中反复使用而不冲突,原因就是虽然它们是同一块虚拟地址,但是它们的页目录,页表,物理页面各不相同,最终映射到不同的物理页面内存单元,最终访问不同的物理页面。但是线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
三、同一个进程下线程共享与独有的数据
在讨论这个问题之前,我们还得回顾一下进程的一个知识点。我们知道,在创建进程的的时候,我们大部分用的是fork()函数,其实还有一个函数就是vfork()函数,vfork()函数创建的子进程与父进程共用同一块虚拟地址空间,它存在的意义就是快速的创建子进程,并且子进程是专门用来运行其它程序的,共用地址空间可以减少子进程数据拷贝父进程的消耗,因此速度快。但它有一个缺陷就是若子进程 return 后,有可能会造成父进程的程序调用栈混乱,这种情况是我们不愿意看到的。看到这里我想你应该知道我要说什么了。那就是多线程共享一块虚拟地址空间与vfork()函数一样,那么它是如何避免这个问题的呢?那就是通过线程之间共享与独有的数据。
共享
- 内存地址空间 (.text/.data/.bss/heap/共享库)
- 文件描述符表
- 信号的处理方式
- 当前工作目录
- 用户ID与组ID
独有
- 线程ID
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
- 寄存器
线程控制原语
注意:使用线程库时gcc指定 –lpthread
-
线程控制
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失 败均直接返回错误号。
参数: pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
参数4:线程主函数执行期间所使用的参数。 -
线程终止
~ 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
~ 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
int pthread_cancel(pthread_t thread); 成功:0;失败:错误号
【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
~ 线程可以调用pthread_exit终止自己。
void pthread_exit(void *retval); 参数:retval表示线程退出状态,通常传NULL -
线程等待:获取指定线程的返回值,并且允许操作系统回收线程资源,一个线程默认启动后处于joinable状态,处于这个状态的线程退出时不会自动释放资源。
int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号
参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态。 -
线程分离:分离一个线程,线程退出后系统自动释放资源,被分离的线程无法被等待。网络、多线程服务器常用。 int pthread_detach(pthread_t thread); 成功:0;失败:错误号
多进程与多线程
敲黑板啦。。既然多进程多线程都可以并发完成任务,哪个好?或者说,既然多进程可以完成,那么为什么还要引入多线程呢?上图~~
由于线程共用一个地址空间,所以线程间通信极为方便,相比于进程而言,线程的创建以及销毁的代价相对较低,进程它是一个线程组,那么对于线程而言,它的执行粒度更为细致。但同时它缺乏访问控制,健壮性低,像一些系统调用都是针对进程的,编写与调试一个多线程程序比单线程程序困难得多,因为线程没有内存隔离,如果单个线程出现问题,那么有可能导致整个程序的退出。
多线程与多进程的应用场景
- 需要频繁创建的销毁的优先使用线程,例如:web服务器。建立一个线程,断了就销毁线程。如果用进程,创建和销毁的代价是很难承受的。
- 如果是CPU密集型程序优先使用线程,程序中都是大量的运算,那么它比较消耗CPU,切换比较频繁,使用线程比较合适。例如:图像处理,算法处理。
- 强相关的程序处理用线程,弱相关的程序处理用进程。例如:一般的server需要完成如下任务:消息收发和消息处理。消息收发和消息处理就是弱相关的任务,而消息处理里面可能又分为消息解码、业务处理,这两个任务相对来说相关性就要强多了。因此消息收发和消息处理可以分进程设计,消息解码和业务处理可以分线程设计。
- 可能扩展到多机分布的用进程,多核分布的用线程。