中断和局部线程存储是如何工作的?
当一个线程用完了分配给它的时间片以后,它不会停止而是再次排队等待。每个处理器在同一时间只能处理一个线程,所以当前线程不得不离开(被从处理器中移出)。然而,在线程跳出执行之前,它得将离开前的状态信息保存下来以便于再次执行。如果你的记性不错,这个功能就称作线程本地存储(TLS).一个线程的本地线程存储包含寄存器,堆栈指针,调度信息,内存中的地址空间以及其他正在使用的资源信息。TLS 中存储的众多寄存器中有一个程序计数器,它会告诉线程下次从哪条指令开始执行。
中断
记得我们曾经说过一个进程不一定需要了解同一台计算机上的其他进程。如果真是这种情况,线程如何知道它将为另外一个进程让路(让出处理器以及其他资源)?这个如噩梦一般的调度决定大多数时候由操作系统处理。Windows自身(终究也是运行在处理器上的一个程序)有一个主线程,称作系统线程,它负责所有其他线程的调度。
Windows通过中断知道它什么时候需要作出线程调度的决定。我们已经用过这个词,但是现在我们要精确地定义什么是中断。中断是一种能够使CPU指令正常执行顺序跳转到计算机内存中的其他地方而不需了解执行程序的内容的结构。Windows决定一个线程执行多长时间并在当前线程的执行顺序中放入一条指令。这个时间在不同系统间甚至同一系统的不同线程间都可能是不同的。由于中断被显式地放入到指令集,所以通常被称为软件中断。一旦中断被设置,Windows就允许线程执行。当线程执行到中断时,Windows使用一个被称为中断处理的特定函数来在TLS中存储线程状态。当前线程的程序计数器会在中断被接收之前存储到TLS中。简单地说,程序计数器就是当前执行指令的地址。一旦线程执行超时,它会按照自身的优先级被移动到线程队列的最后来等待再次被调度。图6是中断过程的介绍:
图6
事实上TLS并没有保存到队列中;它被存储到线程所属进程的内存中。队列中实际存储的是指向那段内存的指针。
如果线程还没有执行完或者线程需要继续执行的话那么这种模式很好。然而,如果线程决定它不需要使用它所有的执行时间会怎样?上下文切换(从一个线程的上下文切换到另外一个线程)的过程在开始时稍微有些不同,但是结果是一样的。一个线程可能需要在下次执行之前等待一个资源。因此,它可能将自己的执行时间放弃给其他线程。这由程序员和操作系统决定。程序员通知线程放弃。线程接下来清除所有Windows 可能在堆栈中放置的中断标志。最后线程模拟一个软件中断。线程存储在TLS中并像之前那样被放到队列的尾部。因为这个概念很容易理解而且和上面的图表很像所以我们将不会为它画图。唯一需要记住的是Windows可能已经在线程堆栈中放入了一个中断。在线程挂起之前这个标志必须被清除;否则,当线程再次执行以后,它可能被无限次中断。当然,具体细节对我们是透明的。程序员不需要为清除这些标志担心。
线程睡眠和时钟中断
正如我们之前说的那样,程序可能已经放弃自己的执行时间并把它留给其他线程以便于自己可能等待一些外部资源。然而,资源可能在线程下次被调度执行之前仍然没有准备好。事实上,它可能在10~20个线程调度执行周期才能准备好。程序员可能希望将线程从执行队列中长时间移出,这样处理器就不需要浪费时间来仅仅为了线程放弃自己的执行时间而反复切换线程上下文。线程自愿地将自己从执行队列中长时间移出的行为称作睡眠。当一个线程进入睡眠状态,它被再次挂起到TLS中,但是这次不会放到TLS执行队列的尾部;它被放到一个单独的睡眠队列中。为了让睡眠队列中的线程能够再次运行,时钟中断会计划好线程何时应该醒来。当一个时钟中断发生且满足在睡眠队列上的某一个线程的唤醒时间时,线程会被移回到可以继续调度执行的运行队列中去。图7描述了这种情况:
图7
线程退出
我们已经探讨过线程中断和线程睡眠。然而,就像生活中所有其他的美好事物一样,线程也必须结束。线程可以在其他线程执行期间按照显式请求停止。当一个线程按照这种方式停止时,它被称作终止。当线程执行完的时候也会停止。不论是哪种情况,只要线程停止了,这个线程的TLS就会被重分配。进程中被线程使用的数据不会丢失,除非进程也结束了。由于进程的数据可能有多于一个线程需要访问所以这是很重要的。线程不能由它们自己终止(指显式调用Abort, 而非自身执行结束);一个线程的终止方法必须从另外一个线程调用。
线程优先级
我们已经知道一个线程可以被中断以便于其他线程可以执行。我们也知道一个线程可能通过放弃一次执行或者让自己进入睡眠状态来放弃执行时间。我们还知道一个线程可以终止。我们最后需要讨论的线程基本概念是线程如何确定自己的优先级。以我们自己的实际生活状态作为例子,我们知道一些任务需要比其他任务有更高的优先级。例如,在本书的面市时间很紧张的时候,作者也需要吃饭。因为吃的欲望,吃饭可能比写书有更高的优先级。还有,如果作者为了写这本书熬夜到很晚,休息系统可能将身体的优先级提高到睡眠。其他人也可能给作者一些任务。然而,那些人可能将他们给作者的任务定了很高的优先级。一些人可以强调某件事很重要,但是重要与否取决于任务接收方是如何区分非常重要与可以等待的任务的。上面的信息包含了很多理论和比喻;然而这与我们的线程概念关联很紧密。一些线程需要更高的优先级。比如吃饭和睡觉的优先级就很高因为它们是我们继续工作的基础(身体是革命的本钱),一些系统任务也有很高的优先级因为计算机需要它们才能起作用。Windows将线程优先级分为0~31,更高的数字意味着更高的优先级。
优先级0只可以由系统设置,它意味着线程处于空闲状态。1~15优先级可以由一个Windows 系统用户设置。如果一个优先级需要设置为高于15,它必须由管理员完成。我们稍后将讨论一个管理员如何完成这个。16~31优先级的线程被认为是实时运行的。当我们说到实时的概念时,通常意味着优先级很高,能够比其他低优先级的线程优先处理。优先处理可能让它们的执行更快速。可能需要实时运行的类型一般是设备驱动,文件系统以及输入设备。想象一下如果你的键盘和鼠标输入不是系统中高优先级的会怎样!用户级别线程的默认优先级是8.
最后要记住的是线程继承它们所在进程的优先级。图8所描述的内容你以后可能要参考。我们也通过这个图标来对0~31更加细分一些。
图8
在一些操作系统中,比如Windows,只要高优先级的线程存在,低优先级的线程就不会被调度执行。处理器将首先调度高优先级的线程。同一个优先级的线程将会按照轮流循环方式执行。当所有高优先级的线程执行完以后,下一个高优先级的线程才会被调度执行。如果又有一个高优先级的线程,所有低优先级的线程被抢占同时处理器的使用权被交给高优先级线程。
管理优先级
基于我们现在掌握的优先级知识,看起来将特定进程优先级设置高一点以便于这个进程中生成的线程可以有更高的可能性被调度执行是必须的。Windows提供了好几种方式来从管理设置或者编程角度设置优先级。现在,我们主要讨论如何管理设置优先级。这可以通过诸如任务管理器以及其他两个成为pview(Visual Studio 自带工具)和pviewer(由Windows NT 或者Windows XP 专业版及以后版本的资源包安装)的工具实现。你也可以使用Windows性能监视器来查看当前优先级。我们现在不会介绍所有的工具。我们将简要地查看如何设置进程的常见优先级。如果你的记性不错,那么应该记得我们一开始介绍进程时通过运行任务管理器来查看当前系统运行的所有进程。我们没有介绍的是可以通过任务管理器窗口提升一个特定进程的优先级。
让我们试着改一个进程的优先级。首先,打开一个应用程序,比如微软表单。现在运行任务管理器并选择进程选项卡。此时表单程序已经作为一个进程运行。右键EXCEL.EXE 并选择面板中的设置优先级。你可以改成自己想要的优先级。把表单程序的优先级设置很高没有意思,关键问题是如果你想改优先级你就可以改。每个进程都有一个优先级同时操作系统不会告诉你你应该设置什么优先级以及不应该设置什么优先级。然而,如果你将要做的事情有不良后果它会给你警告;但是选择权还是在你手里。
在上面的截图中,你可以看到其中的一个优先级前有一个标识。这个标志表示进程当前的优先级。当你设置一个进程的优先级时,你只是设置一个应用程序的实例。这意味着同一个应用程序当前正在运行的实例仍然是默认优先级。近一步,一个程序任何新的实例都会按照默认优先级运行。