playerken

博客园 首页 新随笔 联系 订阅 管理

每隔20ms左右,Windows要查看当前存在的所有线程内核对象。在这些对象中,只有某些对象被视为可以调度的对象。Windows选择可调度的线程内核对象中的一个,将它加载到CPU的寄存器中,它的值是上次保存在线程的环境中的值。这项操作称为上下文转换。

Windows被称为抢占式多线程操作系统,因为一个线程可以随时停止运行,随后另一个线程可进行调度。

 

暂停和恢复线程的运行

在线程内核对象的内部有一个值,用于指明线程的暂停计数。当调用CreateProcess或
CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。这可以防止线程被调度到CPU中。

当线程完全初始化好了之后, CreateProcess或CreateThread要查看是否已经传递了
CREATE_ SUSPENDED标志。如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么该函数将线程的暂停计数递减为0。当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。

一旦改变了线程的环境,必须使线程成为可调度线程。要进行这项操作,可以调
用ResumeThread。单个线程可以暂停若干次。如果一个线程暂停了3次,它必须恢复3次,然后它才可以被分配给一个CPU。当创建线程时,除了使用CREATE_ SUSPENDED外,也可以调用SuspendThread函数来暂停线程的运行。

在实际环境中,调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。如果线程试图从堆栈中分配内存,那么该线程将在该堆栈上设置一个锁。当其他线程试图访问该堆栈时,这些线程的访问就被停止,直到第一个线程恢复运行。只有确切知道目标线程是什么(或者目标线程正在做什么),并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的。

 

暂停和恢复进程的运行

对于Windows来说,不存在暂停或恢复进程的概念,因为进程从来不会被安排获得CPU时间。Windows确实允许一个进程暂停另一个进程中的所有线程的运行,但是从事暂停操作的进程必须是个调试程序。

 

睡眠方式

线程也能告诉系统,它不想在某个时间段内被调度。这是通过调用Sleep函数来实现的。可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。

 

转换到另一个线程

系统提供了一个称为SwithToThread的函数,使得另一个可调度线程。当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫需要CPU时间的线程,SwitchToThread就对该线程进行调度。

调用SwitchToThread函数与调用Sleep是相似的,并且传递给它一个0ms的超时。差别是SwitchToThread允许优先级较低的线程运行。即使低优先级线程迫切需要CPU时间,Sleep也能够立即对调用线程重新进行调度。

 

线程的运行时间

如果是用GetTickCount的话,即是做了一个简单的假设:它不会被中断。但是,在抢占式操作系统中,永远无法知道线程何时被赋予CPU时间。Windows提供了一个称为GetThreadTimes的函数。它可以返回线程执行操作系统代码和应用程序代码用来多少个100ns的CPU时间。

GetProcessTimes返回的时间适用于某个进程中的所有线程。返回的内核时间是所有进程的线程在内核代码中经过的全部时间的总和。

 

运用结构环境

环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的CPU时,它就能够找到它上次中断运行的地方。CONTEXT结构中包含了主机C P U上的每个寄存器的数据结构。在x86计算机上,数据成员是Eax、Ebx、Ecx、Edx等等。

Windows实际上允许查看线程内核对象的内部情况,以便抓取它当前的一组CPU寄存器。若要进行这项操作,只需要调用GetThreadContext函数。

在调用GetThreadContext函数之前,应该调用SuspendThread,否则,线程可能被调度,而且线程的环境可能与你收回的不同。一个线程实际上有两个环境。一个是用户方式,一个是内核方式。GetThreadContext只能返回线程的用户方式环境。如果调用SuspendThread来停止线程的运行,但是该线程目前正在用内核方式运行,那么,即使SuspendThread实际上尚未暂停该线程的运行,它的用户方式仍然处于稳定状态。线程在恢复用户方式之前,它无法执行更多的用户方式代码,因此可以放心地将线程视为处于暂停状态, GetThreadContext函数将能正常运行。

Windows使你能够修改CONTEXT结构中的成员,然后通过调用SetThreadContext将新寄存器值放回线程的内核对象中。同样,修改其环境的线程应该首先暂停,否则其结果将无法预测。

实际上,几乎没有应用程序调用这些函数。增加这些函数是为了增强调试程序和其他工具的功能。

 

线程的优先级

每个线程都会被赋予一个从0(最低)到31(最高)的优先级号码。当系统确定将哪个线程分配给CPU时,它首先观察优先级为31的线程,并以循环方式对它们进行调度。如果优先级为31的线程可以调度,那么就将该线程赋予一个CPU。在该线程的时间片结束时,系统要查看是否还有另一个优先级为31的线程可以运行,如果有,它将允许该线程被赋予一个CPU。只要优先级为31的线程是可调度的,系统就绝对不会将优先级为0到30的线程分配给CPU。

人们可能认为,在这样的系统中,低优先级线程永远得不到机会运行。不过正像前面指出的那样,在任何一个时段内,系统中的大多数线程是不能调度的。

高优先级线程将抢在低优先级线程之前运行,不管低优先级线程正在运行什么。例如,如果一个优先级为5的线程正在运行,系统发现一个高优先级的线程准备要运行,那么系统就会立即暂停低优先级线程的运行(即使它处于它的时间片中),并且将CPU分配给高优先级线程,使它获得一个完整的时间片。

当系统引导时,它会创建一个特殊的线程,称为0页线程。该线程被赋予优先级0,
它是整个系统中唯一的一个在优先级0上运行的线程。当系统中没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲RAM页面置0。

 

对优先级的抽象说明

• Microsoft没有将调度程序的行为特性完全固定下来。
• Microsoft没有让应用程序充分利用调度程序的特性。
• Microsoft声称调度程序的算法是变化的,在编写代码时应有所准备。

Windows API展示了系统的调度程序上的一个抽象层,这样就永远不会直接与调度程序进行通信。

Windows 支持6个优先级类:即空闲、低于正常、正常、高于正常、高和实时。当然,正常优先级是最常用的优先级类, 99%的应用程序均使用这个优先级类。

当系统什么也不做的时候,将空闲优先级类用于应用程序的运行是最恰当不过的。定期更新系统的某些状态的统计信息跟踪应用程序不应该干扰关键任务的运行。只有当绝对必要的时候,才可以使用高优先级类。Windows Explorer是在高优先级上运行的。大多数时间Explorer的线程是暂停的,等待用户按下操作键或者点击鼠标按钮时被唤醒。如果低优先级线程正在运行,系统会立即抢在这些线程的前面,让Explorer的线程优先运行。MicroSoft就是按这种方法设计Explorer的,因为用户希望无论系统中正在运行什么,外壳程序都具有极强的响应能力。

应该尽可能避免使用实时优先级类。实时优先级是很高的优先级,它可能干扰操作系统任务的运行,因为大多数操作系统线程均以较低的优先级来运行。因此实时线程可能阻止必要的磁盘I/O信息和网络信息的产生。此外,键盘和鼠标输入将无法及时得到处理,用户可能以为系统已经暂停运行。大体来说,必须有足够的理由才能使用实时优先级,比如需要以很短的等待时间来响应硬件事件,或者执行某些不能中断的短期任务。

 

程序的优先级

创建子进程的进程负责选择子进程运行的优先级类。一旦子进程运行,它就能够通过调用SetPriorityClass来改变它自己的优先级类。

通过将线程的相对优先级与线程的进程优先级类综合起来考虑,系统就可以确定线程的优先级等级。有时这称为线程的基本优先级等级。系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。系统只能为基本优先级等级在1至15之间的线程提高其优先级等级。实际上这是因为这个范围称为动态优先级范围。

当用户对进程的窗口进行操作时,该进程就称为前台进程,所有其他进程则称为后台进程。为了提高前台进程的响应性,Windows能够为前台进程中的线程调整其调度算法。系统可以为前台进程的线程提供比通常多的CPU时间量。这种调整只能在前台进程属于正常优先级类的进程时才能进行。如果它属于其他任何优先级类,就无法进行任何调整。

 

亲缘性

按照默认设置,当系统将线程分配给处理器时,如果所有其他因素相同的话,它将设法在它上次运行的那个处理器上运行线程。让线程留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。

计算机在引导时,系统要确定机器中有多少个CPU可供使用。按照默认设置,任何线程都可以调度到这些CPU中的任何一个上去运行。为了限制在可用C P U的子集上运行的单个进程中的线程数量,可以调用SetProcessAffinityMask。子进程可以继承进程的亲缘性。此外,可以使用作业内核对象将一组进程限制在要求的一组CPU上运行。

在大多数环境中,改变线程的亲缘性就会影响调度程序有效地在各个CPU之间移植线程的能力,而这种能力可以最有效地使用CPU时间。

posted on 2011-10-20 22:00  playerken  阅读(1306)  评论(0编辑  收藏  举报