一、线程简介
早期的计算机系统只允许一个任务独占系统资源, 一次只能执行一个程序。由于对程序并发执行的需求,引入了多进程。进程的引入可以解决多任务支持的问题,但是也产生了新的问题:每个进程分别分配资源开销比较大,进程频繁切换导致额外系统开销,进程间的通信实现复杂。考虑现实中的场景:
一个word程序如果采用多进程,一个进程负责界面交互,一个进程负责后台运算,会相当低效 (进程通信不好实现 进程频繁切换导致额外的系统开销)。一个同时要处理大量请求的网络数据库如果采用多进程,对每个请求都创建一个进程去响应那服务器的资源很快就耗尽了,而且进程切换消耗很大。
由此就演化出了在一个进程的内存空间上开辟多个"小进程",利用这些小进程来实现多个任务的方法,这些小进程就是所谓的线程。这些线程在进程的内存空间内共享很多进程的资源,所以每个线程分配资源开销不会很大。线程的规模较小,切换开销也不会很大。线程之间共享进程的一部分地址空间,线程之间的通信也不会很麻烦。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
线程是进程里面的一个执行序列,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。同一个进程中的多个线程可以并发执行,一个线程可以创建和撤销另一个线程,但是线程不能够独立执行,必须依存在进程中。每个进程运行时都会创建一个主线程,也叫主控线程,通过主控线程可以继续创建其他子线程。
进程是资源管理的最小单位,线程是程序执行调度的最小单位。
进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。线程既可以通过内核来实现 也可通过用户态来实现。因为线程的管理者可以是用户也可以是操作系统本身,线程是进程内部的东西,当然存在由进程直接管理线程的可能性,因此线程的实现就应该分为内核态线程实现和用户态线程实现。
多线程之间切换消耗资源少,但是不稳定 一个线程崩溃了会影响整个进程;多进程之间切换消耗资源多,但是稳定 一个进程崩溃不会影响其他进程。
协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程,一个线程也可以拥有多个协程。 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。可以在线程内并发执行,又不会引起安全问题。
二、线程资源
线程间共享的资源:内核区:文件描述符表,每种信号的处理方式,当前工作目录;用户区:堆区,数据区( bss段,Data段Test 段)
线程间独占的资源
1.线程ID
每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
2.寄存器组的值
线程间是并发运行的,每个线程有自己不同的运行上下文,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程重新切换时能恢复。
4.错误返回码
同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以不同的线程应该拥有自己的错误返回码变量。
5.线程的信号屏蔽码
每个线程感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6.线程的优先级
线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
7.线程的栈
一个栈中只有最下方的帧可被读写,相应的也只有该帧对应的那个函数被激活,处于工作状态。为了实现多线程必须绕开栈的限制。为此在创建新的线程时, 要为这个线程建新的栈,每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务。多线程的进程在内存中有多个栈,多个栈之间以固定的区域隔开,以备栈的增长。每个线程可调用自己栈下方的帧中的参数和变量。
三、线程切换
(1)一般的进程切换分为两步 :1)切换页目录使用新的地址空间;2)切换内核栈和硬件上下文。对于Linux来讲,地址空间是线程和进程的最大区别,如果是线程切换的话,不需要切换页目录使用新的地址空间。但是切换内核栈和硬件上下文则是线程切换和进程切换都需要做的。
(2)切换进程上下文:
进程上下文可以分为三个部分:
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
系统中的每一个进程都有自己的上下文。一个正在使用处理器运行的进程称为当前进程。当前进程因时间片用完或者因等待某个事件而阻塞时,进程调度需要把处理器的使用权从当前进程交给另一个进程,这个过程叫做进程切换。此时,被调用进程成为当前进程。在进程切换时系统要把当前进程的上下文保存在指定的内存区域(该进程的任务状态段TSS中),然后把下一个使用处理器运行的进程的上下文设置成当前进程的上下文。当一个进程经过调度再次使用CPU运行时,系统要恢复该进程保存的上下文。进程的切换也就是上下文切换。
(3)线程切换:
Linux下的线程实质上是轻量级进程。线程生成时会生成对应的进程控制结构,只是该结构与父线程的进程控制结构共享了同一个进程内存空间。同时新线程的进程控制结构将从父线程(进程)处复制得到同样的进程信息,如打开文件列表和信号阻塞掩码等。创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对在进程之间切换,在线程之间切换所需的时间更少,因为后者不包括地址空间的切换。
线程上下文切换的原理与此类似,只是线程在同一地址空间中,不需要MMU等切换,只需要切换必要的CPU寄存器,因此,线程切换比进程切换快的多。
四、线程的用户级和内核级
进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。但是线程的管理者可以是用户也可以是操作系统本身,因此线程的实现分为内核态线程实现和用户态线程实现。
线程是进程的不同执行序列,也就是说线程是独立运行的基本单位,也是CPU调度的基本单位。那么操作系统是如何实现管理线程的?
首先操作系统像管理进程一样维护线程的所有资源,将线程控制块存放在操作系统的内核空间中,此时操作系统就同时掌管进程控制块和线程控制块。操作系统管理线程的好处是:
1.用户编程简单;
2.如果一个线程执行阻塞操作,操作系统可以从容的调度另外一个线程的执行。
内核线程的实现缺点是:
1.效率低,因为线程在内核态实现,每次线程切换都需要陷入到内核由操作系统来调度,而由用户态切换到内核态要花很多时间。另外内核态实现会占用内核稀有的资源,因为操作系统要维护线程列表,操作系统所占内核空间一旦装载后就无法动态改变,并且线程的数量远远大于进程的数量,随着线程数的增加内核将耗尽;
2.内核态的实现需要修改操作系统。
用户态是如何实现管理线程的?用户态管理线程就是用户自己做线程的切换,自己管理线程的信息,操作系统无需知道线程的存在。在用户态下进行线程的管理需要用户创建一个调度线程。一个线程在执行完一段时间后主动把资源释放给其他线程使用,而在内核态下则无需如此,因为操作系统可通过周期性的时钟中断把控制权夺过来