线程
线程是一种轻量的进程。它共享进程中的所有资源,包括打开文件描述符、信号、子进程等。。但是也拥有自己的堆栈、寄存器和程序计数器。的程序计数器,寄存器,堆栈,分别用来记录接着执行哪条指令、保存工作变量、保存了已调用但还未返回的过程。 与进程相比,线程的特点:
- 同一进程中的各个线程共享进程的同一地址空间和其他各种资源,包括
- 创建线程的速度比较快。
- 线程间的切换较进程的切换容易。
线程模型
线程与进程拥有的资源对比如下:
每个进程中的内容 | 每个线程中的内容 |
---|
地址空间 | 程序计数器 | 全局变量 | 堆栈 | 打开的文件描述符 | 寄存器 | 子进程 | 状态 | 定时器 | | 信号与信号处理程序 | | 账户信息 | |
在下图中,可以看到三个传统的进程。每个进程含有一个控制线程和自己的地址空间。右边的图则是一个进程含有三个线程的情况,这三个线程共享进程的资源但是拥有自己的堆栈、程序计数器、寄存器。
每个线程拥有自己的堆栈是因为线程以执行某个函数作为开始,因此不同的线程需要自己的堆栈储存函数调用的相关信息,包括自动变量和函数的返回地址等。
线程有如下常用的操作
- 创建新的线程
进程通常以一个线程开始,之后可以创建出更多的线程。 - 结束线程
线程完成它的工作之后,会调用系统调用退出,并返回状态。 - 等待另外一个线程退出
阻塞以等待另外一个线程退出。 - 放弃CPU
线程库无法使用时钟中断强制线程让出CPU,为了防止线程无限占据CPU,因此需要系统调用让线程自动放弃CPU从而让另外一个线程运行。
几种实现
有几种用于实现线程包的方法。分别是
为了能在进程中切换线程,线程也像进程一样需要一个用于保存线程信息的线程表。
用户空间
线程在用户空间实现,由用户空间的运行时系统来调度运行。用户空间中每个进程会储存一张线程表,该表含有线程运行所需要信息。 当线程陷入本地阻塞时,如等待另外一个线程的输入,运行时系统会切换线程。切换线程时会保存当前线程状态,接着载入要切换的线程状态,然后执行指令。因为线程实现于用户空间,因此对于内核来说,它并不知道线程的存在,所以不会对线程有任何特殊的处理。 因此他有如下优点:
- 切换线程由运行时系统决定,不会用到系统调用,因此比较快。系统调用可能陷入内核、进行上下文切换、刷新高速缓存,因此会比较慢。
- 线程的调度完全由运行时系统决定,因此对线程的调度有着极高的可扩展性。
上下文切换:上下文切换指内核在cpu上切换进程或者线程。上下文切换会先保存当前任务寄存器的状态到堆栈中,接着再装载要切换的任务的状态到cpu上(包括程序计数器),然后跳转到程序计数器所指位置执行指令。 上下文切换发生在以下几种情况:
- 任务因执行系统调用而陷入阻塞。
- 任务正常终止。
- 任务用完给它分配的时间片。
- 硬件中断。
- 用户代码挂起当前任务,比如线程执行yieId()方法,让出cpu
缺点:
- 内核不知道线程的存在,当某个线程因系统调用而阻塞的时候,该进程中的所有线程都会陷入阻塞。
解决方法:1)当线程会因为系统调用而陷入阻塞的时候,会通知运行时系统。运行时系统会利用此消息安排其他的线程工作。2)把系统调用改成非阻塞的。 - 若运行的线程发生缺页故障时,其他线程也无法被调度。
缺页故障:程序所执行的指令不在内存中而是在磁盘中,因此需要在磁盘中取回缺失的指令放入内存。
- 当一个线程工作时,除非它主动放弃cpu,否则其他线程无法运行。一种可能的解决方法是频繁的时钟中断,但这太耗费系统资源。
内核空间
线程在内核空间实现,线程表存于内核中,由内核调度线程。 优点:
- 线程阻塞后,内核可以调用另外一个线程而不是阻塞其他的线程。
缺点:
- 切换线程在内核中完成,需要更多的时间。
- 创建或撤销线程的代价过大。
可能解决思路:1)回收线程,当撤销线程时,给它设置一个标志位。之后,创建新的线程可以利用之前使用过的线程。
混合实现
将内核线程与用户空间的线程相结合。 用户空间的线程复用内核线程,内核只会调用内核线程。
调度程序激活机制
调度程序模拟机制用于模拟内核线程,但提供了用户空间线程包的灵活和更好的性能。 调度程序模拟机制的几个想法:
- 当用户线程使用的系统调用时是安全的,那么不应该使用非阻塞或者提前检查。(这里的安全指的是用户线程所处的状态是安全的)
- 当线程陷于内核或者发生缺页故障的时候,如果有那么就应该调度其他线程。
- 用户线程因等待另一个线程的输出而阻塞时,在用户空间切换线程即可,不用经过内核。
具体实现: 内核给每个进程分配一个虚拟处理器。 当一个用户线程调用系统函数或缺页导致阻塞时,内核获取这一信息后,会通过已知的地址调用用户运行时系统,并传递用户线程号和事件的描述,运行时系统收到该信息后会进行相应的调度。 当之前阻塞的线程又能够重新运行时,内核以相同的方式通知运行时系统,接着,运行时系统进行调度。 内核调用运行时系统称作上行调用。一般,由上层调用下层的函数,但这里下层调用了上层的函数,这违反了分层次系统的内在结构的概念。
弹出式线程
一个消息的到达导致创建一个新的线程,这种线程称作弹出式线程。
单线程代码多线程化
单线程多线程化使用全局变量会出现竞争,进而导致程序结果错误。 单线程代码转为多线程时有如下问题:
- 全局变量。
解决思路:1)不使用全局变量。这可能会与之前的程序发生冲突。2)为每个线程创建一个唯一的全局变量,线程调用的所有过程都能获得该全局变量。这里提供三种方案:传递内存块、__thread关键字、线程特有数据。 - 函数是不可重入的。不可重入指:线程中断返回后,状态与中断前不一致。这可能因为另外一个线程更改了公用的变量。
- 堆栈管理。当进程的堆栈溢出时,内核会为进程的堆栈分配更多的空间。但同样的情况不会出现在线程的堆栈上。
|