一、线程基本概念
一个进程在一个时间只能干一件事情,如果需要执行多件事情,必须将进程进行一个分割,让它和分身术一样,在一个时间处理多件事情,那么线程的概念产生了。
(一)定义
线程
,又被称为轻量级进程,是操作系统CPU调度执行的最小单位
,是进程内部的一条执行流,是比进程更小能独立运行的基本单位。
线程不拥有系统资源
,线程会共享
进程的系统资源,线程只有在运行中必不可少的资源,如栈,寄存器等。
在线程模式下,一个进程至少有一个线程,但也可以有多个线程
将一个进程分为多个线程,则可以让不同线程同时运转在不同的处理器上,从而提高进程的执行速度。
我们可以看一个例子,我们使用Word时,实际上就是打开了多个线程,这些线程一个负责显示,一个接受输入,一个定时进行存盘。这些进程一起运转,让我们感受到我们的输入和屏幕显示同时发生,如下图所示:
(二)实现
一个进程可以包含多个线程,main函数执行的线程称为主线程
,主线程是进程执行的入口。C语言利用函数来组织一组指令,在创建线程时,需要指定线程的执行序列,即函数,故称为函数线程
。
主线程创建第一个线程,这个线程也可以创建其他线程,这几个线程会并发执行。
我们在C语言函数实现函数调用时,只需要传入函数名即可。而函数线程,我们需要给定一个函数地址,告诉创建的线程从哪个函数开始执行
。如
void* fun(void* arg)
int main()
{
pthread_create(fun);//此处的fun是函数地址,表示创建出来的线程执行这个函数。
}
(三)优点
多线程方式在同一时刻可以处理各自独立的任务,这种方法的好处有很多:
通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码
,每个线程在进行事件处理时可以采用同步编程模式,同步编程模式要比异步编程模式简单得多。- 多个进程必须要使用操作系统提供的复杂机制才能是西安内存和文件描述符的共享,
而多个线程自动的可以访问相同的存储地址空间和文件描述符。
- 有些问题可以通过将其分解从而改善整个程序的吞吐量,在只有一个控制线程的情况下,单个进程需要完成多个任务时,实际上需要把这些任务串行化;有了多个控制线程,
相互独立的任务的处理就可以交互进行,只需要为每个任务分配一个单独的线程
,当然只有在处理过程互不依赖的情况下,两个任务的执行才可以穿插进行。 - 交互的程序同样可以通过多线程实现响应时间的改善,
多线程可以把程序中处理用户输入输出的部分与其他部分分开
。
(四)线程和进程的区别
我们简单列出几点:
- 进程是资源分配的最小单位,线程CPU调用的最小单位。
- 进程有自己的独立地址空间,线程共享进程中的地址空间。
- 进程的创建消耗资源大,线程的创建相对较小。
- 进程的切换开销大,线程的切换开销相对较小。
二、线程管理(对资源的管理)
线程管理就是要维持线程的各种信息
,这些信息包含了线程的各种关键资料,存放这些信息的数据结构称为线程控制表或线程控制块。
线程共享一个进程空间,因此很多资源是共享的,这些共享的资源显然不需要存放在线程控制块中,而是存放在进程控制块
。线程是不同的执行序列(指令),总会有些不能共享的资源;类似于一个家庭很多东西都是共享的,如冰箱,厨房等,但每个人都有自己的私有物品,如牙刷,毛巾等。这些不被共享的资源和信息都需要存放在线程控制块里。
创建线程的目的就是要经常协作,共享是追求的方向,所以我们判断什么信息可以放到线程控制块,什么可以放到进程控制块的规律就是:应当让共享的资源越多越好。
那么评判标准就是:如果某资源不独享会导致线程运行错误,则该资源就由每个线程独享;而其他资源都由进程里面的所有线程共享。
按照这个标准来划分,一般情况下线程共享的资源和独享的资源如下表所示:
线程共享资源 | 线程独享资源 |
---|---|
地址空间 | 程序计数器(PC)用于存放下一条指令所在单元的地址的地方 |
代码区,数据区,堆 | 寄存器 |
文件描述符 | 栈 |
子进程,信号等信息 | 状态字 |
不能共享的资源都是线程的上下文,即运行环境,一旦共享,不能正常实现业务功能。
三、操作系统线程实现方式
对线程管理有两种:一种是让进程自己来管理线程,第二种是让操作系统来管理线程。故就出现了内核态线程和用户态线程。
由进程自己管理的就是用户态线程实现,由操作系统管理就是内核态线程实现。
我们从来没有讨论过进程的实现方式,原因是:
进程是在CPU上实现并发,而CPU是由操作系统管理的,因此,进程的实现只能由操作系统内核来进行
,而不存在用户态实现的情况。
(一)内核态线程实现
操作系统管理线程,实现内核态线程,要保持维护线程的各种资料,即将线程控制块存放在操作系统内核空间
,这样操作系统内核就同时有进程控制块,和线程控制块,如下图所示:
每个用户线程就是一个内核线程,线程的是实现在内核态,内核知道用户态有几条线程,用户态线程和内核态线程数目对应,一种n-n关系
。表示线程是由内核支持的。操作系统可以对线程进行各种类似进程的管理,如线程调度,线程的资源分配,出现阻塞进行处理等措施。
这种内核态线程有优点也有缺点:
优点:
用户编程保持简单,因为线程的复杂性由操作系统承担
,用户程序员在编程时无需管理线程的调度即无需担心执行,挂起等操作。- 如果一个线程执行阻塞操作,操作系统可以从容的调度另一个线程执行,
因为操作能够监控所有的线程
。 - 线程之间无需合作得到CPU控制权,因为
操作系统通过周期性的时钟中断把控制权夺过来
,重新分配。
缺点:
会占用内核稀缺的内存资源
,一旦内核空间溢出,操作系统将停止运转。切换效率低
,每次线程切换都需要陷入内核态。- 需要修改操作系统,加入线程管理。
(二)用户态线程实现
用户态线程实现,需要用户自己写一个执行系统作为调用都,即线程的实现在用户态完成的,由线程库进行线程的创建,销毁等操作。在用户态线程,每一个线程都是同一权限下的,不会出现夺走CPU控制权的现象,就会出现一个线程占用CPU时间过长,故线程之间必须进行合作,对CPU控制权进行分配,
如下图所示:
内核感知不到线程,只知道它是一个进程,是一种1-n的关系,这也表示该系统不支持线程,只知道有进程存在
。
这种用户态线程有优点也有缺点:
优点:
灵活性
,内核不用知道线程的存在,所以在任何操作系统上都能应用。线程切换快
,因为切换在用户态进行,无需陷入内核态。不用修改操作系统,实现简单
。
缺点:
编程程序变得诡异
,因为用户态线程需要合作才能运转,所以我们需要考虑什么时候让出CPU给别的线程使用,这个时机的选择就很难了。- 如果
一个线程阻塞
,操作系统只会看到进程阻塞,故把CPU控制权交给另一个进程,这样则会造成整个进程阻塞
。 用户程序相对复杂
。
(三)组合级线程实现
用户态和内核态都存在缺陷,现代操作系统使用的是将两者结合起来。用户态的执行系统负责进程内部线程在非阻塞是的切换;内核态的操作系统负责阻塞线程的切换,
将这种方式称作组合级线程。
其中内核态线程数量较少,而用户态线程数量多,每个内核态线程可以服务一个或多个用户态线程,换句话说,用户态线程被多路复用到内核态线程上,
如下图所示:
共有3条线程,将其分为两组,一组2个,一组1个,每一组线程使用一个内核线程 ,这样,该进程使用两个内核线程,形成n-m的关系,
如果一个线程阻塞,则与其一组的线程皆阻塞,但另外一组可以继续运行。
所以在我们分配线程时:
- 将需要执行阻塞操作的线程设为内核态线程。
- 不会阻塞操作的线程设为用户态线程。
这样我们便可以获得用户态,内核态线程的优点,避免缺点。
四、Linux系统线程实现方式
Linux实现线程的机制非常独特。从内核角度来说,它没有线程的概念,没有为线程单独提供机制,Linux把所有线程都当作进程来实现。
内核并没有准备特别的调度算法或定义特别的数据结构来表征进行。线程仅仅被视为一个与其他进程共享某些资源的进程。
每个进程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通进程(只是线程和其他进程共享某些资源,如进程的地址空间 .data .bss .text .heap)
。
轻量级线程这种叫法本身就概括了Linux和其他系统的差别:
- 在其他系统中,相当于重量级进程,线程被抽象成一种耗费较少资源,运行速度的执行单元。
- 而对于Linux来说,它只是一种进行间共享资源的手段。
举个例子来说就是:假如我们有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向四个不同线程的指针的进程描述符,该描述符负责描述地址空间,打开的文件等共享资源,线程本身再去描述它独占的资源。而Linux仅仅创建四个进程并分配四个普通的task_struct内核栈结构,建立这四个进程时指定他们共享某些资源就可以实现线程
。
创建线程、创建进程调用的内核底层函数是一样的:传入参数标志调用系统调用函数clone,由clone调用do_fork实现,只不过传入的参数标志不一样
。fork函数指定父子进程需要共享的信息;创建线程函数指定共享的CLONE_FILES文件标志、CLONE_FS文件系统标志等共享的信息。
所以说:
Linux没有专门的线程模块,它只是通过实现进程,在创建进程时,传递一些参数,使得我们创建的进程和其他进程共享某些资源,如地址空间,变量等资源,这样我们就说这个进程不是进程了,而是一个线程。要注意这种实现方式和操作系统的完全不一样。
五、Linux系统线程库的使用
调用clone函数,传入某些共享的参数,将这个系统调用进行封装为创建线程库函数,再将其他系统调用进行分装,就是Linux系统上的线程库,线程库函数主要包括:创建线程函数pthread_create,它的系统调用就是clone
;其他的库函数都调用了对应的系统调用。