都说操作系统是用户体验驱动其发展的,在很久很久的Micrisoft的16位Windows操作系统中,那是单线程而且是不能抢占的CPU的操作系统,这样导致了当某个线程发生死锁或者不能正确的运行的时候,整个操作系统都不能运行,处于一种冻结的状态。用户只能无奈的按下Reset按钮来进行重启。这样会导致之前运行的所有的数据都会丢失。因此,新的内核就被设计出来了。--我只是知识的搬运工
在新的OS内核中,进程实际是应用程序的实例要使用的资源的集合。每个进行都被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据不会被另外一个进行所访问。而线程的职责是对CPU进行逻辑的虚拟化。
线程的开销
在创建,销毁一个线程的过程中,存在着一些巨大的开销。这些开销有时间上,也有空间上面的。
线程内核对象(Thread-kernel object)
这种数据结构包含着一组对线程进行描述的属性,这些属性一般是用于CPU的调度,例如线程的优先级,等待的时间等等。同时还包括了线程的上下文(Thread Contexgt),这是CPU寄存器集合的内存块所存放的信息的副本。
线程环境块(thread environment block)
TEB是在用户模式中分配的初始化的内存块。TEB耗用一个内存页(4KB)。TEB包含着异常处理链首。线程进入的每个try块都在链首插入一个节点;线程推出try块是,从链表中删除该节点。TEB还包含着线程的“线程本地存储”数据,以及由GDI和OpenGL图形使用的一些数据结构。
用户模式栈(User-model stack)
这就是常说的用户栈,用于存放传给方法的形参和方法中自定义的实参,已经当前方法返回的时候,线程应该从那个地方开始执行。Windows的默认用户模式栈的大小为1M。
内核模式栈
当应用程序代码想操作系统中的内核模式函数传递实参时,还会使用内核模式栈。出于对安全的考虑,所有由应用程序向内核函数传递的参数,都会先复制到内核模式栈中。由于程序不能访问内核模式栈,所以参数一经复制过去内核模式栈中,程序便不能修改其值。其实这个内核模式栈在功能上跟用户模式栈的作用是一样的,都是用于存储传给方法的形参,已经方法中自定义的实参,以及函数的返回地址。特别之处就是,应用程序不能修改里面的值,只能由内核函数进行修改,从而达到了安全。
DLL线程连接和线程分离通知
上面所说的三个开销都是对于内存的开销,这个DLL线程连接和线程分离通知却是时间上面的开销。不过这也是相对的来说的,因为上面的三个开销,在分配内存,初始化内存过程中,也必须花费很多的时间。在创建一个新的线程的时候,Windows都会调用进程中加载的所有非托管DLL的DllMain方法,并向这个方法传递一个DLL_THREAD_ATTCH标志。类似的,终止线程的时候,也会调用这个DllMain方法,并传递一个DLL_THREAD_DETACH标志。因为有些DLL需要获取这些通知,才能为进程中创建/销毁的每个线程执行特殊的初始化或者清理操作。事实上,每个进程都会加载很多非托管的DLL文件,所以初始化或者销毁一个线程,便需要调用多个的DllMain方法。
CPU调度时上下文切换的开销
上下文切换的开销主要集中在两个方面:
- 把一个线程从CPU中移除,然后根据一定的调度算法,用分派器选择某个线程,在上下文切换器中进行切换。
- 如果该切换的线程的代码和数据还在RAM中,就必须从RAM中读取数据到Cache和CPU寄存器中。
不使用ThreadPool而自己创建一个线程的原则
一般情况下,要为不会阻止其他线程的相对较短的任务处理多个线程并且不需要对这些任务执行任何特定调度时,使用 ThreadPool 类是一种最简单的方式。 但是,有多个理由创建您自己的线程:
- 如果您需要使一个任务具有特定的优先级。
- 如果您具有可能会长时间运行(并因此阻止其他任务)的任务。
- 如果您需要将线程放置到单线程单元中(所有 ThreadPool 线程均处于多线程单元中)。
- 如果您需要与该线程关联的稳定标识。 例如,您应使用一个专用线程来中止该线程,将其挂起或按名称发现它。
- 如果您需要运行与用户界面交互的后台线程,.NET Framework 2.0 版提供了 BackgroundWorker 组件,该组件可以使用事件与用户界面线程的跨线程封送进行通信。
线程的优先级
在Windows中,线程的优先级是从0(最低)-31(最高)的。一般CPU是根据线程的优先级来调度线程的。如果当前调度的线程的线程的优先级是10,如果突然线程的就绪队列中,来了一个优先级为15的线程,那么操作系统会强制的进行线程的切换,来马上调度优先级为15的线程,这被称为抢占式调度。
在实际编程中,我们是看不到这0-31的优先级的,这是因为Windows只是公开了这些优先级的一个抽象。特别要说明的的是,线程的优先级由进程的优先级类和线程的相对优先级来决定。
进程优先级类 |
|
|
|
|
|
|
相对线程优先级 |
Idle |
Below Normal |
Normal |
Above Normal |
High |
Realtime |
Time-Cirtical |
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 |
6 |
8 |
11 |
22 |
Idle |
1 |
1 |
1 |
1 |
1 |
16 |
这里要特别说明的是,Windows为自己保留了优先级0和Realtime范围,同时CLR也为自己保留了Idle和Time-Cirtical范围的优先级。程序的线程绝对优先级是由进程优先级类和相对线程优先级所共同决定的,同时我们不应该去更改进程的优先级。因为在正常情况下,进程根据其启动它的进程来分配优先级。大多数进程都是由Windows资源管理器启动,后者在Normal优先级类中生成他们的所有子进程。所以我们在Thread.Priority中只有Highest,Above Normal,Noraml,Below Normal,Lowset这几种,相对应的优先级是6,7,8,9,10。
图1 Spy++中查看Chrome的某个线程的信息
前台线程和后台线程
前台线程和后台线程的主要区别是,进程中只要存在没有完成的前台线程,进程就不会被销毁。换句话说就是,如果所有的前台线程都完成了,进程就会被销毁,即使是存在未完成任务的后台线程。我们可以通过设置Thread.IsBackground来把线程设置为前台线程或者是后台线程。通过ThreadPool(其中ThreadPool.QueueUserWorkItem,Timer,Task等等都是通过ThreadTool来实现的)来实现的线程都是后台线程,通过Thread类来实现的线程都是前台线程。