您的位置:首页 > 编程语言

Windows编程【7】小结

2012-04-30 00:07 204 查看

第七章 线程调度、优先级和关联性

1、抢占式操作系统必须使用某种算法确定对何时应对哪些线程进行调度,时间又为多长。该章讲述MS的Vista使用的调度算法。

2、每个线程都有一个上下文,后者保存在线程的内核对象中。这个上下文反映了线程上一次执行时CPU寄存器的状态。大概每隔20ms,Windows都会查看所有当前存在的线程内核对象。对可调度的线程,采取一定的算法选取一个,将其上次保存在线程上下文中的值载入CPU寄存器。这一操作被称为上下文切换(context switch)。Windows实际上会记录每个线程运行的次数。

3、无法保证线程总在运行,线程会获得整个处理器,系统将不允许运行其他线程等限制。

4、有些线程的挂起计数大于0,这意味着该线程已被挂起,不应该给它调度任何CPU时间。可以通过调用CreateProcess或者CreateThread函数传入CREATE_SUSPENDED标志来创建一个被挂起的线程。还有SuspendThread和ResumeThread这两个函数。)

一、线程的挂起和恢复

1、线程内核对象中有一个值表示线程的挂起计数。调用CreateProcess或者CreateThread创建时,系统会将线程内核对象挂起计数初始化为1。这样就不会给这个线程调度CPU了。这正是我们所希望的,因为线程初始化需要时间。在线程初始化之后,CreateProcess或者CreteThread函数将查看是否有CREATE_SUSPENDED标志传入。

2、如果ResumeThread函数成功,它将返回线程的前一个挂起计数;否则,它将返回oxffffffff。一个线程被挂起多少次,就要被恢复多少次。

3、任何线程都可以调用这个函数挂起另一个线程(只要有线程的句柄)。当然,线程也可以将自己挂起,但是它无法自己恢复。SuspendThread也返回线程之前的挂起计数。一个线程最多可以挂起MAXIMUM_SUSPEND_COUNT(WinNT.h定义为127)次。应用程序在调用SuspendThread时必须小心,因为试图挂起一个线程时。我们不知道线程在做什么。例如,如果线程正在分配堆中的内存,线程将锁定堆。

二、进程的挂起和恢复

1、Windows中不存在挂起和恢复进程的概念,因为系统从来不会给进程调度CPU时间。在一个特殊情况下,即调试器处理WaitForDebugEvent返回的调用事件时,Windows将冻结被调试进程中的所有线程。直至调试器调用ContinueDebugEvent。

2、Windows没有提供其他方式挂起进程中的所有线程,因为存在竞态条件问题。例如,在线程被挂起时,可能创建一个新的进程。系统必须想方设法挂起这个时间段中任何新的线程。Windows已经将这个功能集成到系统的调试机制中了。

3、作者用ToolHelp32来创建快照遍历的时候,也有问题,就是在枚举线程集合的时候,会有新的线程被创建,也可能有线程被销毁。

三、睡眠

1、线程还可以告诉系统,在一段时间内自己不需要调度了。可以调用Sleep实现。

2、调用Sleep函数,将使线程资源放弃属于它的时间片中剩下的部分。

3、系统设置线程不可调度的时间只是“近似于”所设定的毫秒数。如果是100ms,那么线程可能睡眠差不多这么长时间,但是可能会长达数秒甚至数分钟。实际情况取决于系统中其他线程的运行请。

4、调用Sleep并传入INFINITE参数,这是在告诉系统永远都不要调度这个进程。这样做没有什么用处。

5、可以给Sleep传入0,这是在告诉系统,主调线程放弃了时间片的剩余部分,它强制系统调度其他线程。但是系统有可能重新调度刚刚调用了Sleep的那个线程。如果没有相同或者较高优先级的可调度线程时,就会发生这样的事情。

四、切换到另一个线程

1、系统提供一个名为SwitchToThread的函数,如果存在另一个可调度线程,那么系统会让此线程运行。调用这个函数时,系统查看是否存在正急需CPU时间的饥饿线程。如果没有SwitchToThread立即返回。如果存在,SwitchToThread将调度该线程(其优先级可能比SwitchToThread的主调线程低)。饥饿线程可以运行一个时间量,然后系统调度程序恢复正常运行。通过这个函数,需要某个资源的线程可以强制一个可能拥有该资源的低优先级的线程放弃资源。

2、调用SwitchToThread与调用Sleep类似,传入0ms超时即可。区别在于,SwitchToThread运行执行低优先级线程,Sleep会立即重新调度主调线程,即使低优先级线程还处于饥饿状态。

五、在超线程CPU上切换到另一个线程

1、所有线程共享主要的执行资源,比如CPU高速缓存。当一个线程中止时,CPU自动执行另一个线程,无需操作系统干预。只有在缓存未命中、分支预测错误和需要等待前一个指令的结果等情况下,CPU才会暂停。

六、线程的执行时间

1、在代码的执行不会被中断的前提下,可以调用GetTickCount64来判断代码执行用时。参考GetTickCount功能即可知道。

2、在Windows Vista之前,就有一个函数能够返回这种信息了,既GetThreadTimes,能返回4个不同的时间值。



3、GetProcessTimes这个函数类似于GetThreadTimes,可以用于进程中的所有线程(即使线程已经终止)。例如:所返回的内核时间是所有线程在内核模式下所耗时间的总和。

七、在实际上下文中谈CONTEXT结构

1、CONTEXT结构成员的具体情况取决于Windows运行在什么CPU上。事实上,在Windows定义的所有数据结构中,CONTEXT结构是唯一一个特定于CPU的。CPU上的每个寄存器它都有一个对应的数据成员。

2、CONTEXT结构分为几部分:控制寄存器、整数寄存器、浮点寄存器、段寄存器、调试寄存器、扩展寄存器。

3、Windows实际上允许我们查看线程的内核对象的内部,并获取当前CPU寄存器状态的集合。为此,只需要调用GetThreadContext这个函数。其中分配的CONTEXT结构,需要初始化一些标志(结构的ContextFlags成员)以表示要获取哪些寄存器。调用该函数前,应该先调用SuspendThread,否则,系统可能正好获得调度此线程,这样一来,线程的上下文与所获取的信息就不一致了。一个线程实际上有两个上下文:用户模式和内核模式。GetThreadContext只能返回线程的用户模式上下文。

4、Windows还运行我们通过调用SetThreadContext来改变结构中的成员,并把新的寄存器值放回线程的内核对象中。如果要改变哪个线程的上下文,应该先暂停该线程,否则结果无法预料。(这可能会造成远程线程的访问违规。)同样,必须初始化CONTEXT的ContextFlags成员。

八、线程优先级

1、每个线程都被赋予0(最低)~31(最高)的优先级数。当系统确定给哪个线程分配CPU时,它会首先查看优先级31的线程,并以循环(round-robin)的方式进行调度。

2、只要有优先级为31的线程可供调度,系统就不会给优先级0~30的线程分配CPU。这种情况成为饥饿。当较高优先级的线程占用了CPU时间,致使较低优先级的线程无法运行时,我们就成这种情况为饥饿。在多处理器机器上饥饿发生的可能性要小得多,因为这种机器上优先级31的线程和优先级为30的线程可以同时运行。

3、较高优先级的线程总是会抢占较低优先级的线程,无论较低优先级的线程是否正在执行。(会立即暂停较低优先级的线程,即使其时间片还没有用完),并将CPU分配给较高优先级的线程,该线程获得一个完整的时间片。

4、系统启动时,将创建一个名为页面清零线程(zero page thread)的特殊线程。这个线程的优先级为0,而且是整个系统中唯一一个优先级为0的线程,在没有其它进程需要执行的时候,将系统内存中的所有闲置页面清零。

九、从抽象角度看优先级

1、MS随着系统用途的改变不断修改调度算法,但是,却能保证软件开发人员编写的系统能在其未来版本上运行。其原因如下:

a、MS没有再文档中完整描述调度程序的行为。

b、MS不允许应用程序充分利用调度程序的特性。

c、MS明确告知用户,调度算法会发生变化,使应用软件程序员知道应该防御性地编程。

2、我们设计一个应用程序的时候,应该考虑用户可能会同时运行其他什么程序。然后需要根据应用程序中线程的响应性选择一个优先级类。

3、Windows支持6个优先级类(priority class):idle,below normal,normal,above normal,high和real-time。当然,normal是最常用的优先级类,为99%的应用程序所使用。

表7-2

4、只有在绝对必要的时候才使用high优先级类。应该尽可能避免使用real-time优先级类。因为real-time优先级级别最高,可以影响操作系统的任务。大多数操作系统线程在执行时所用的优先级类都比real-time低。因此,real-time线程甚至可以阻止必需的磁盘I/O和网络通信。而且,键盘和鼠标输入也无法及时得到处理,用户可能会认为系统死机了。基本上,只有充分的理由,才能使用real-time优先级。例如需要响应延时很短的硬件事件,或者执行一些不能中断的非常短命的任务。

5、进程不能运行在real-time优先级类,除非用户有Increase Scheduling Priority(提高计划优先级)特权。默认情况下,隶属于管理员或者高级用户组的用户都具有这一权限。

6、在Windows2000中,MS又增加了两个优先级类:below normal和above normal,因为有些公司抱怨已有的优先级类灵活性不够。

7、选择了优先级后,就不需要考虑应用程序与其它应用程序的关系了,应该转而关注自己应用程序里的线程。Windows支持7个相对线程优先级:idle,lowest,below normal,normal,above normal,highest和time-critical。这些优先级是相对于进程优先级的。同样大多数线程使用normal线程优先级。



8、概括起来,进程都属于某个优先级类,另外而已指定进程中线程的相对线程优先级。应用程序的开发人员无需处理优先级(0~31),而是由系统将进程的优先级类和线程的相对优先级映射到一个优先级值。这个映射正是MS不想做什么承诺的地方。事实上,这个映射在操作系统的不同版本中已经发生了变化。



9、系统不允许其它任何线程的优先级为0,因为0优先级保留给页面清零线程了。而且,应用程序也无法获得一些优先级。当然,如果编写的是运行在内核模式的设备驱动程序,那么就可以获得这些优先级。real-time优先级类的线程,其优先级值不能低于16。同理,非real-time优先级的线程的优先级值不能高于15。

10、进程永远无法调度,能调度的是线程。进程优先级是MS提出的一个抽象概念。

十、优先级编程

1、调用CreateProcess时,可以在fdwCreate参数中传入需要的优先级。



2、创建子进程的进程会选择子进程运行的优先级,听起来有些奇怪。不过,一旦进程运行,便可以通过调用SetPriorityClass来改变自己的优先级。该进程句柄参数必须要足够的访问权限。

3、可以用GetPriorityClass来获取进程优先级。

4、通过命令行界面雕饰程序优先级设定。

5、当线程最开始创建时,它的线程优先级总是设置为normal。使用SetThreadPriority可以设置相对线程优先级。

6、Windows并没有提供返回函数线程优先级的函数,(获得线程的相对优先级的函数GetThreadPriority)这是MS故意的。MS保留了任何时候改变调度算法的权力。

7、系统通过线程的相对优先级加上线程所属进程的优先级来确定线程的优先级值。有时候,这也被称为线程的基本优先级值(base priority level)。偶尔,系统也会提升一个线程的优先级,通常是为了响应某种I/O事件比如窗口消息或者磁盘读取。

8、在系统偶尔提升了一个线程的优先级后,会在接下来的时间片中没过一个时间片就递减1优先级,直至线程的基本优先级。可以留意到,线程的当前优先级不会低于线程的基本优先级。

9、MS没有在文档中记录任何一个设备驱动程序能够将线程的优先级提升多少。因此,MS可以不断地微调动态提升,以确定最佳的总体响应性。

10、系统只提升优先级值在1~15的线程。这个范围被称为动态优先级范围(dynamic priority range)。而且,系统不会把线程的优先级提升到实时范围(高于15)。

11、有些开发人员抱怨系统的动态提升功能对线程性能有不利影响。MS允许我们禁止系统对线程优先级进行动态提升。SetProcessPriorityBoost运行或禁止系统提升一个进程中所有线程的优先级;SetThreadPriorityBoost则允许或禁止提升某个线程的优先级。这两个函数都有对应的函数用来判断是否启动优先级提升:GetProcessPriorityBoost和GetThreadPriorityBoost。

12、另一种情况也会导致系统动态提升线程的优先级。既是有低优先级线程处于饥饿状态3到4秒时。系统会动态将饥饿线程的优先级提升15,并允许该线程允许两个时间片。

13、如果用户需要使用某个进程的窗口,这个进程就称为前台进程(foreground process),而所有其他的进程称为后台进程(background process)。为了改进前台进程的响应性,Windows会为前台进程中的线程微调调度算法。系统给前台进程的线程分配比一般情况下更多的时间片。这种微调只在前台进程是normal优先级时才进行。如果处于其它优先级,则不会进行微调。

14、如果一个低优先级线程获得CPU时间,它可以很轻松地在很短时间内将成百甚至成千个I/O请求入列。因为I/O请求一般都需要时间进行处理,可能低优先级线程会挂起高优先级的线程,使他们无法完成任务,从而显著影响系统的响应性。比如说,在执行一些运行时间较长的低优先级服务磁盘碎片整理程序、病毒扫描程序、内容索引程序等的时候,机器的响应性会变得很差。

15、从Vista开始,线程可以在进行I/O请求时设置优先级了。可以通过调用SetThreadPriority并传入THREAD_MODE_BACKGROUND_BEGIN来告诉Windows应该发送低优先级的I/O请求。注意,这也将降低线程的CPU调度优先级。传入THREAD_MODE_BACKGROUND_END可以进行normal优先级I/O请求(以及normalCPU调度优先级)。系统不允许线程改变另一个线程的I/O优先级。

16、同样的,可以让进程中的所有线程都进行低优先级的I/O请求和低CPU调度。SetPriorityClass。系统也不允许线程改变另一个进行中线程的I/O优先级。

17、SetFileInformationByHandle设置的优先级将覆盖进程的优先级或者线程,即分别通过SetPriorityClass或者SetThreadPriority设置的优先级。

十一、关联性

1、默认情况下,Vista在给线程分配处理器时,使用软关联(soft affinity)。意思是如果其他因素都一样,系统将使线程在上一次运行的处理器上运行。让线程始终在同一个处理器上运行有助于重用仍在处理器高速缓存中的数据。

2、有一种称为NUMA(Non-Uniform Memory Access,非同一内存访问)的计算机体系结构,结构的计算机由多个系统版(board)组成,每个系统版都有自己的CPU和内存板块。



3、NUMA系统在CPU只访问自己所在系统版上的内存时,可达到最佳性能。如果CPU需要访问其他系统版上的内存,性能会下降得很厉害。为了支持这种体系结构,Vista运行我们设置进程和线程的关联性(affinity)。也就是说,我们可以控制CPU让哪些CPU运行特定的线程。这称为应关联(hard affinity)。

4、系统在启动时将确定计算机中存在多少个CPU,应用程序可以通过调用GetSystemInfo来查询机器上的CPU的数量。默认情况下,系统可以将任何CPU调度给任何线程使用,如果要限制某些线程只在可用CPU的一个子集上运行,则可以调用SetProcessAffinityMask。注意的是,子进程将继承进程关联性。也可以使用作业内核对象来限制一组进程只能再一组CPU上运行。

5、当然,还有一个函数可以获得进程的关联性掩码,即GetProcessAffinityMask。

6、系统的关联性掩码表示系统中哪个CPU可以运行这些线程。进程的关联性掩码总是系统的关联性掩码的一个真子集。

7、SetThreadAffinityMask可以设置各线程的关联性掩码。不过必须是进程关联性掩码的真子集。

8、当一个x86系统启动时,系统将执行代码,检查主机上的哪个CPU存在著名的Pentium浮点bug。系统必须对每个CPU做这项检查。检查的方法是,将一个线程的关联性设置为该CPU,执行可能会出错的除法操作,然后比较结果是否与已知正确的结构相符。随后采取同样的步骤检查下一个CPU,以此类推。

9、为了更高效地使用CPU时间,调度程序可能会在多个CPU之间迁移线程。在大多数环境里,改变线程的关联性,将妨碍调度程序的这种能力。如下以此唤醒ABC,C线程无法运行,本来是可以抢占A的CPU0的。



10、我们可以告诉系统,我们想让一个线程运行在一个CPU上,但系统运行将它移到另一个空闲的CPU。SetThreadIdleProcessor函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: