第二十六章 线程基础
目录:
26.1 Windows为什么要支持线程
26.2 线程开销
26.3 停止疯狂
26.4 CPU发展趋势
26.5 CLR线程和Windows线程
26.6 使用专用线程执行异步的计算限制操作
26.7 使用线程的理由
26.8 线程调度和优先级
26.9 前台线程和后台线程
26.10 继续学习
Windows为什么要支持线程
进程实际是应用程序的实例要使用的资源的集合。每个进程都被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问。
作为一个Windows概念,线程的职责是对CPU进行虚拟化。Windows为每个进程都提供了改进程专用的线程(功能相当于一个CPU)。应用程序的代码进入死循环,与那个代码关联的进程回“冻结”,但其他进程不会冻结,会继续执行。
线程开销
线程允许用户强制终止似乎已冻结的应用程序。和一切虚拟化机制一样,线程有空间(内存耗用)和时间(运行时的执行性能)上的开销。
线程内核对象:OS为系统中创建的每个线程都分配并初始化这种数据结构之一。数据结构包含一组对线程进行描述的属性。数据结构还包含所谓的线程上下文。上下文是包含CPU寄存器集合的内存块。
线程环境块(TEB):TEB是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。TEB耗用1个内存页。TEB包含线程的异常处理链首。线程进入的每个try块都在链首插入一个节点;线程退出try块时从链中删除改节点。TEB还包含线程的“线程本地存储”数据,以及由GDI和OpenGL图形使用的一些数据结构。
用户模式栈:用户模式栈存储传给方法的局部变量和实参。它包含一个地址;指出当前方法返回时,线程应该从什么地方接着执行。
内核模式栈:应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。
DLL线程连接(attach)和线程分离(detach)通知:Windwos的一个策略是,任何时候在进程中创建线程,都会第哦啊用进程中加载的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_ATTACH标志。任何线程终止,都会调用进程中的所有非托管DLL的DllMain方方法,并向方法传递DLL_THREAD_DETACH标志。
Windows任何时刻只将一个线程分配给一个CPU。那个线程能运行一个“时间片(也称为“量”或“量程”)”的长度。时间片到期,Windows就上下文切换到另一个线程:
1.将CPU寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
2.从现有线程集合中选出一个线程供调度。如果该线程由另一个进程拥有,Windows在开始执行任何代码或者接触任何数据之前,还必须切换CPU“看见”的虚拟地址空间。
3.将所选上下文结构中的值加载到CPU的寄存器中。
上下文切换时净开销;所产生的开销不会换来任何内存或性能上的收益。所以说上下文切换时通过牺牲性能来换取更好的用户体验。
执行上下文切换所需的时间取决于CPU架构和速度。而填充CPU缓存所需要的时间取决于系统中运行的应用程序,CPU缓存的大小以及其他各种因素。
执行垃圾回收时,CLR必须挂起(暂停)所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历它们的栈,在恢复所有线程。
停止疯狂
CPU发展趋势
多个CPU
超线程芯片
多核芯片
CLR线程和Windows线程
CLR使用Windows的线程处理功能,CLR线程完全等价于Windows线程。
使用专用线程执行异步的计算限制操作
满足以下条件就可以创建自己的线程:
线程需要以非普通线程优先级运行。
需要线程表现为要给前台线程,防止应用程序在线程结束任务前终止。
计算限制的任务需要长时间运行。
要启动线程,并可能调用Thread的Abort方法来提前终止它。
使用线程的理由
可响应性(通常是对于客户端GUI应用程序)
Windows为每个进程提供它自己的线程,确保发生死循环的应用程序不会妨碍其他应用程序。
性能(对于客户端和服务器应用程序)
由于Windows每个CPU调度一个线程,而且多个CPU能并发执行这些线程,所以同时执行多个操作能提升性能。
线程调度和优先级
抢占式操作系统必须使用算法判断在什么时候调度哪些线程多长时间——线程可在任何时间停止(被抢占)并调度另一个线程。(每个线程的内核对象包含一个上下文结构,上下文结构反映了线程上一次执行完毕后CPU寄存器的状态。在一个时间片之后,Windows检查现存的所有线程内核对象。在这些对象中,只有那些没有正在等待什么的线程才适合调度。Windows选择一个可调度的线程内核对象,并上下文切换到它。Windows实际记录了每个线程被上下文切换到的次数。然后,线程开始执行代码,并在其进程的地址空间处理数据。又过过了一个时间片之后,Windows执行下一次上下文切换。Windows从系统启动开始便一直执行上下文切换,直到系统关闭为止。)
每个线程都分配了从0到31的优先级。饥饿:较高优先级的线程占用了太多CPU时间,造成较低优先级的线程无法运行。
系统启动时会创建一个特殊的零页线程,该线程的优先级是0,而且是整个系统唯一优先级为0的线程。在没有其他线程需要“干活儿”的时候,零页线程将系统RAM的所有空闲页清零。
进程优先级类:Idle,Below Normal,Above Normal,High,Realtime.
相对线程优先级:Idle,Lowest,Below Normal,Normal,Above Normal,Highest,Time-Critical.
相对线程优先级 | 进程优先级类 | |||||
Idle | Below Normal | Normal | Above Nomal | High | Realtime | |
Time-Critical | 15 | 15 | 15 | 15 | 15 | 31 |
Highest | 6 | 8 | 10 | 12 | 15 | 26 |
Above Normal | 5 | 7 | 9 | 11 | 14 | 25 |
Normal | 4 | 6 | 8 | 10 | 13 | 24 |
Below Normal | 3 | 5 | 7 | 9 | 12 | 23 |
Lowest | 2 | 4 | 3 | 8 | 11 | 22 |
Idle | 1 | 1 | 1 | 1 | 1 | 16 |
前台线程和后台线程
CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程的所有前台线程停止运行时,CLR强制终止仍在运行的任何后台线程。这些后台线程被直接终止,不抛出异常。
应用程序的主线程以及通过构造一个Thread对象来显式创建的任何线程都默认为前台线程,线程池线程默认为后台线程。由进入托管执行环境的本机代码创建的任何线程被标记为后台线程