标签:style blog http color 使用 strong
2014-06-28
Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例。进程不过是应用程序的一个实例要使用的资源的一个集合。每个进程都赋予了一个虚拟地址空间,确保一个进程使用的代码和数据无法由另一个进行访问。这样就确保了应用程序集的健壮性,因为一个进程无法破坏另一个进程里的数据和代码。另外,进程是无法访问到OS的内核代码和数据。
如果一个应用程序进入死循环时,如果只是单核的CPU的,它会无限循环执行下去,不能执行其他代码,这样会使系统停止响应。对此,Microsoft拿出的一个解决方案——线程。线程的职责就是对CPU的虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU,可将线程理解成一个逻辑CPU)。如果应用程序的代码进入无限循环,与那个代码关联的进程会"冻结",但其他进程不会冻结,会继续执行。
线程尽管非常强悍,但和一切虚拟化机制一样,线程会产生空间(内存耗用)和时间(运行时的执行性能)上的开销。
注意:C#和其他大多数托管编程语言生成的DLL没有DllMain函数,所有不会接到通知,这提升了性能。
单CPU每次只能做一件事情,所以,Windows必须在系统中的所有线程之间共享CPU。在给定的时刻,Windows只将一个线程分配给CPU。那个线程允许运行一个“时间片”。一旦时间片到期,Windows就上下文切换到另一个线程。每次上下文切换都要求Windows做以下操作:
上下文切换完成后,CPU执行所选的线程,直到它的时间片到期。然后,会发生另一次上下文切换。Windows大约每30毫秒执行一次上下文切换。上下文切换是净开销;也就是说,上下文切换所产生的开销不会换来任何内存或性能上的收益。Windows执行上下文切换,向用户提供一个健壮的、响应灵敏的操作系统。
事实上,上下文切换对性能的影响可能超出你的想象:
根据上述讨论,我们的结论是必须尽可能地避免使用线程,因为它们要耗用大量内存,而且需要相当多的时间来创建、销毁和关联。WIndows在进行上下文切换,以及垃圾回收时也会浪费更多的时间。但是不可否认,因为才是Windows变得更健壮,反应更灵敏。
应该指出,安装多个CPU的计算机可以真正同时允许几个线程,这提升应用程序的可伸缩性(在更少的时间内做更多的事)。Windows为每个CPU内核都分配一个线程,每个内核都自己执行到其他线程的上下文切换。Windows确保单个线程不会同时在多个内核上调度。
如果追求性能,那么任何计算机最优的线程数就是那台计算机的CPU个数。如果线程数超过了CPU的个数那么就会发生线程上下文切换和性能损失。
在Windows中,创建一个进程的代价是昂贵的。创建一个进程通常要花几秒钟的时间,必须分配大量的内存,这些内存必须初始化,EXE和DLL文件必须从磁盘上加载等等。相反,在Windows创建线程是十分廉价的。所以,开发人员决定停止创建进程,改为创建线程。这就是我们看到有这么多线程的原因。但是,线程相对于其它系统资源还是比较昂贵的,所以还是应该省着用。
必须承认,系统中的大多数线程都是本地代码创建的。所以,线程的用户模式栈仅仅保留(预定)地址空间,而且极有可能没有完全提交来获取物理内存。然而,随着越来越多的应用程序成为托管应用程序,或者在其中运行托管组件,会有越来越多的栈被完全提交,会真实的分配到1MB的物理内存。无论如何,即使抛开用户模式栈不谈,所有线程仍然会分配到内核模式栈以及其它资源。这种觉得线程十分廉价便胡乱创建线程的势头必须停止。
CLR使用的是Windows的线程处理能力。虽然今天,CLR线程直接对应于一个Windows线程,但Mircrosoft CLR团队保留了将来把它从Windows线程分离的权利。有一天,CLR可能引入它自己的逻辑线程,使一个逻辑线程并非映射到一个物理Windows线程。据说,逻辑线程将使用比物理线程少得多的资源,所以能在极少量的物理线程上运行大量的逻辑线程。遗憾的是,CLR团队还没有推出这个功能。
对你来说,这一切意味着在操纵线程时,代码应尽可能少地做出一些假设。例如,应避免P/Invoke本地Windows函数,因为这些函数对CLR线程一无所知。通过避免使用本地Windows函数,坚持使用FCL中的类型,将来性能的到提升之后,你的代码马上就能享受到这种提升。
备注:如果想P/Invoke本地代码,而且代码必须使用当前物理操作系统的线程来执行,那么应该调用System.Threading.Thread的静态BeginThreadAffinity方法。BeginThreadAffinity就是告诉CLR不要切换线程。线程不再需要使用物理操作系统线程运行时,可调用Thread的EndThreadAffinity方法来通知CLR。
本节将展示如何创建一个线程,并让它执行一次异步计算限制操作。虽然会教你具体如何做,但是强烈建议你避免采用这里展示的技术。相反,应该尽量使用CLR的线程池来执行异步计算限制操作,具体以后会讨论。
如果执行的代码要求处于一种特定的状态,而这种状态对于线程池的线程来说是非比寻常的,就可以考虑创建一个线程 。例如,满足以下任意一个条件,就可以显式创建自己的线程:
为了创建一个专用线程,要构造一个System.Threading.Thread类的一个实例,向它传递方法的名称,它的构造器如下:
1 public sealed class Thread : CriticalFinalizerObject, _Thread 2 { 3 public Thread(ParameterizedThreadStart start){ } 4 //这里没有列出一些不常用的构造器 5 }
ParameterizedThreadStart委托的签名如下:
public delegate void ParameterizedThreadStart(object obj);
下面的代码演示如何创建一个专用线程,让它异步调用一个方法:
1 public void AsynchronousThreadDemo() 2 { 3 Console.WriteLine("Main thread: starting a dedicated thread " + 4 "to do an asynchronous operation"); 5 Thread dedicateThread = new Thread(ComputeBoundOp); 6 dedicateThread.Start(5); 7 8 Console.WriteLine("Main thread: Doing other work here..."); 9 Thread.Sleep(5000); 10 11 dedicateThread.Join();//等待线程终止 12 Console.WriteLine("Main exit"); 13 } 14 15 private void ComputeBoundOp(object state) 16 { 17 //这个个方法由一个专用线程执行 18 Console.WriteLine("In ComputeBoundOp: state={0}", state); 19 Thread.Sleep(1000);//模拟其他任务(1秒) 20 //这个方法返回后,专用线程终止 21 }
运行程序,可能得到下面的输出:
Main thread: starting a dedicated thread to do an asynchronous operation
Main thread: Doing other work here...
In ComputeBoundOp: state=5
Main exit
但有的时候运行上述代码,也可能得到以下结果,因为我无法控制Windows对两个线程进行调度的方式:
Main thread: starting a dedicated thread to do an asynchronous operation
In ComputeBoundOp: state=5
Main thread: Doing other work here...
Main exit
返回
抢占式(preemptive)操作系统必须使用某种算法判断在什么时候调度哪些线程多长时间。本节讨论Windows采用的算法。在前面,已经提到过每个线程的内核对象都包含一个上下文结构。上下文结构反映了当线程上一次执行时,线程的CPU寄存器的状态。在一个时间片之后,Windows检查现有的所有线程内存对象。在这些对象中,只有那些没有正在等待什么的线程才适合调度。Windows选择一个可调度的线程内核对象,并上下文切换到它。Windows实际记录了每个线程被上下文切换到的次数。可以使用向Microsoft Spy++(Visual studio的一个小工具)这样的工具查看这个数据。
Windows之所以被称为一种抢占式多线程操作系统,是因为线程可以在任何时间被停止(被抢占),并调度另一个线程。所以,你不能保证自己的线程一直在运行,不能阻止其他线程的运行。
每个线程都分配了从0(最低)到31(最高)的优先级。系统决定将哪个线程分配给一个CPU时,它首先检查优先级为31的线程,并以一种轮流的方式调度它们。如果优先级为31的线程是可调度的,就把它分配给一个CPU。这个线程的时间片结束时,系统检查是否有另一个优先级为31的线程可以运行;如果是,就允许将那个线程分配给一个CPU。
只要系统中存在一个可调度的优先级为31的线程,系统就永远不会将优先级0~30的任何线程分配给CPU。这种情况称为饥饿(starvation)。
较高优先级的线程总是抢占较低优先级的线程,例如:一个优先级为5的线程正在运行,而系统确定一个较高优先级的线程准备好运行,系统会立即挂起(暂停)较低优先级的线程(即使后者的时间片还没有用完),将CPU分配给较高优先级的线程,该线程将获得一个完整的时间片。
顺便说一下,系统启动时,会创建一个零页线程(zero page thread)的特殊线程。这个线程的优先级为0,而且是整个系统中唯一一个优先级为0的线程。零页线程负责在没有其他进程需要执行时,将系统的RAM的所有空闲页清零。
设计应用程序时,应决定自己的应用程序是需要比机器上同时运行的其他应用程序更大还是更小的响应能力。然后选择一个进程优先级类(priority class)来反映你的决定。Windows支持6个进程优先级类:Idle,Below Normal,Normal,Above Normal,High和Realtime。Normal是默认的进程优先级类,所以它也是最常用的进程优先级类。一个应用程序(比如屏幕保护程序)在系统什么事情都不做的时候运行,就适合分配Idle优先级类。只有在绝对必要时才使用High优先级类。Realtime优先级类要尽可能避免,它的优先级相当高,甚至可能干扰操作系统任务,除了需要响应延迟(latency)很短的硬件事件,或一些执行不能中断的非常“短命”的任务。
选好一个优先级类之后,就不要再思考你的应用程序和其他应用程序的关系了。现在,要把注意力放在应用程序中的线程上。Windows支持7个相对线程优先级:Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical。这些优先级是相对进程优先级类的。同样,Normal是默认的优先级。
总之,你的进程是一个优先级类的成员。在你的进程中,要为各个线程分配相对优先级。事实上,0~31的线程优先级,是由进程的优先级类和其中的一个线程的相对优先级映射而来的。下图展现了这种映射关系:
线程相对 优先级 |
进程优先级类 | |||||
---|---|---|---|---|---|---|
Idle |
Below Normal |
Normal |
Above Normal |
High |
Real-Time | |
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 |
6 |
8 |
11 |
22 |
Idle |
1 |
1 |
1 |
1 |
1 |
16 |
请注意,表中线程优先级没有为0的。这是因为0优先级保留给零页线程了,系统不允许其他线程的优先级为0。而且,以下优先级也是不可获得的:17,18,19,20,21,27,28,29和30。当然,如果编写的是运行在内核模式的设备却、驱动程序,可以获得这些优先级。
注意:"进程优先级类"的概念容易引起一些混淆。人们可能认为这意味着Windows能调度进程。然而,Windows永远不会调度进程;它调度的只有线程。"进程优先级类"是Microsoft提出的一个抽象概念,旨在帮助你理解自己的应用程序和其它正在运行应用程序的关系,它没有其它用途。
提示:最好是降低一个线程的优先级,而不是提升另一个线程的优先级。
在你的应用程序中可以更改它的线程的相对线程优先级,这需要设置Thread的Priority属性,向它传递ThreadPriority枚举类型中定义的5个值之一,即Lowest(最低),Below Normal(低于标准),Normal(标准),Above Normal(高于标准),Highest(最高)。CLR为自己保留了Idle和Time-Critical优先级。
应该指出的是,System.Diagnostics命名空间包含一个Process类和一个ProcessThread类。这两个类分别提供了进程和线程的Windows视图。应用程序需要以特殊的安全权限运行才能使用这两个类。例如,在Silverlight应用程序或者ASP.NET应用程序中,就不可以使用这两个类。
另一方面,应用程序可使用AppDomain和Thread类,它们公开了AppDomain和线程的CLR视图。一般不需要特殊安全权限来使用这两个类,虽然某些操作仍需要提升权限才可以。
CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程中的所有前台线程停止时,CLR会强制终止仍然在运行的任何后台进行。这些后台进程被直接终止,不会抛出异常。
因此,前台进程应该用于执行确实想完成的任务,比如将数据从内存缓存区fluch到磁盘。另外,应该为非关键的任务使用后台线程,比如重新计算电子表格的单元格,或者为记录建立索引。这是由于这些工作能在应用程序重启时继续,而且如果用户终止应用程序,就没有必要强迫它保持活动状态。
CLR要提供前台线程和后台线程的概念来更好地支持AppDomain。每个AppDomain都可以运行一个单独的应用程序,每个应用程序都有它自己的前台线程。如果一个应用程序退出,造成它的前台线程终止,则CLR仍然需要保持活动并运行,使其他应用程序继续运行。所有应用程序都退出,它们的所有前台线程都终止后,整个进程就可以被销毁了。
在一个线程的生存期,任何时候可以从前台变成后台,或者从后台变成前台。应用程序的主线程以及通过构造一个Thread对象来显式创建的任何线程都默认为前台线程。另一方面,线程池默认为后台线程。此外,由进入托管执行环境的本地(native)代码创建的任何线程都被标记为后台线程。
下面的代码演示了前台线程和后台线程的差异:
1 static void Main() 2 { 3 //创建一个新线程(默认为前台线程) 4 Thread t = new Thread(Worker); 5 6 //是线程成为一个后台线程 7 t.IsBackground = true; 8 9 //启动线程 10 t.Start(); 11 12 //如果t是一个前台线程,则应用程序大约10秒后才终止 13 //如果t是一个后台线程,则应用程序立即终止 14 Console.WriteLine("Return form Main."); 15 } 16 private static void Worker() 17 { 18 Thread.Sleep(10000); //模拟工作10秒 19 20 //下面这一行代码,只有由一个前台线程执行时,才会显示出来 21 Console.WriteLine("Return form Worker."); 22 }
重要提示:要尽量避免使用前台线程。作者有一次接受一个顾问工作,有个应用程序就是不终止。花了几小时研究问题后,才发现是一个UI组件显示的创建了一个前台线程(默认),这正是进程一直不终止的原因。后来修改组件用了线程池,从而解决了问题。执行效率也提升了
[1] 《CLR via C#》笔记——线程基础 http://www.cnblogs.com/xiashengwang/archive/2012/07/20/2601108.html
《CLR via C#》读书笔记 之 线程基础,布布扣,bubuko.com
标签:style blog http color 使用 strong
原文地址:http://www.cnblogs.com/Ming8006/p/3813596.html