你也会注意到任务管理器中有CPU使用率的信息。这是因为进程也有一个使用计算机处理器的执行顺序。这个执行顺序就是线程。这个线程由CPU上正在使用的寄存器,线程使用的堆栈以及保存线程当前状态的存储器共同定义。存储器和堆栈的概念对那些经常处理底层内存分配的同僚们来说应该很熟悉;然而,对.NET Framework 中的堆栈来说,你可以把它看成一块用来快速访问数据,存储值类型或者指向对象、方法参数以及每个方法调用的本地数据的内存区域。
单线程进程
正如上面提到的,每个进程至少有一个执行顺序或(线程。创建一个进程包括让进程开始运行指令。初始线程又称作原始线程或主线程。线程的实际执行顺序由你的程序中的代码决定。例如,在一个简单的.NET Windows 窗体应用程序中,主线程在你的工程中德静态Main()方法处启动。它开始于对Application.Run()的调用。
现在我们大概知道了什么是一个进程以及它至少有一个线程,让我们从图2中的虚拟模型角度看看这种关系。
图2
让我们看一下上面的图,你会发现线程和数据在同一个隔离区间内。这是要说明你在进程中定义的数据可以被线程访问。线程在处理器上执行同时按需要使用进程中的数据。这看起来很简单;我们有一个物理隔离的进程,所以其他进程不可能修改这个数据。在这个进程看来,它是系统中正在运行的唯一进程。我们不需要知道其他进程的细节以及它们的关联线程就可以让我们自己的进程工作。
说得更准确一些,线程实际上是指向一个进程指令流的一个指针。线程本身并不包含指令,它只是通过由数据和分支决策确定的指令指出了当前以及未来可能的路径。
时间片
当我们讨论多任务时,我们指出操作系统为每个程序分配一定时间,然后中断当前运行程序并允许另外一个程序执行。这并不完全准确。处理器实际上为进程分配时间。进程可以执行的时间被称作“时间片”或者“限量”。时间片的间隔对程序员和任何非操作系统内核的程序来说都是变化莫测的。程序员不应该在他们的程序中将时间片的值假定为一个常量。每个操作系统和每个处理器都可能设定一个不同的时间。
不过,我们之前还没有提到一个涉及并发的潜在问题,而且我们应该考虑如果每个进程都是物理隔离的,那么并发将如何发挥作用。这意味着挑战才刚刚开始,这也是本书的余下部分要重点关注的。我们提到过一个进程在运行时至少有一个线程。我们的进程在任意一个时间点都可能有多于一个任务。例如,它可能需要通过网络访问一个SQL Server 数据库,同时需要绘出用户接口。
多线程的进程
你可能已经知道,我们可以将分配给进程的时间片拆开来用。这是通过在进程内额外生成新线程来实现。你可能想生成一个额外线程来做一些后台的工作,比如访问一个网络或者查询一个数据库。因为这些子线程通常被创建来做一些工作,所以它们通常被称作工作线程。这些线程共享分配给进程的与系统中其他进程隔离开的内存空间。在一个进程内生成新的线程通常被称作自由线程。
自由线程的概念相对于单元线程模型有很大的优势,后者用在Visual Basic 6.0 中。在单元线程中,每个进程都被分配一份它需要的全局数据的拷贝来执行。每个生成的线程都在它自己的进程中生成,以便于线程之间不能共享进程的内存中的数据。我们通过对比来看一下这些模型的差异。图3描述了单元线程概念,而图4描述了自由线程概念。我们不需要在这里花费太多时间,但是理解这些差异对我们很重要:
图3
图4
正如你看到的,每次当你想做一些后台的工作时,它一般在它自己的进程中进行。因此这又称作在进程内运行。这个模型与图4中的线程模型非常不同。
我们得到了使用额外线程的好处以及共享同样数据的能力。要注意一个重要的问题:在同一个时间点只能有一个线程在处理器上执行。进程中的每个线程会在被分配的执行空间内来进行工作。让我们再看一遍图5中的图表来帮助我们理解它是如何工作的。
图5
为了便于理解,本书中使用的例子和图表都假设系统中只有一个处理器。然而,如果计算机中有超过一个处理器的话,你的应用程序使用多线程会高效一些。因为操作系统有两个位置(处理器)来执行线程的代码了。还是用我们之前提过的银行的例子,这类似于我们让一个新出纳开一个新窗口。操作系统负责决定哪个线程运行在哪个处理器上。如果程序员选择,.NET 平台提供控制一个进程使用哪个CPU的功能。这通过System.Diagnostics.Process.ProcessorAffinity属性实现。需要注意的是,这个属性是设置在进程级别的以至于在这个进程中的所有进程都会执行在同样的处理器上。
调度这些线程要比上一张图标中描述的复杂很多,但是就我们的目的来说这已经足够了。由于每个线程都按顺序执行,我们可能被银行工作人员提醒要在银行柜台前排队。我们也要记住这些线程都会在一个很短的时间内被中断。同时,另外一个线程,可能是同一进程里的,也可能是其他进程里的,会开始执行。在我们继续探讨之前,先看看任务管理器。
运行任务管理器并选中进程选项卡。打开了以后,选择查看->选择列面板。你将会看到任务管理器中的列。我们在这里只关心一个列-线程数。选中了以后,你将看到类似以下的内容:
单击确认以后你将看到很多进程有不止一个线程。这证实了你的程序在一个进程中可能有多个线程的想法。
中断和局部线程存储是如何工作的?
当一个线程用完了分配给它的时间片以后,它不会停止而是再次排队等待。每个处理器在同一时间只能处理一个线程,所以当前线程不得不离开(被从处理器中移出)。然而,在线程跳出执行之前,它得将离开前的状态信息保存下来以便于再次执行。如果你的记性不错,这个功能就称作线程本地存储(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 并选择面板中的设置优先级。你可以改成自己想要的优先级。把表单程序的优先级设置很高没有意思,关键问题是如果你想改优先级你就可以改。每个进程都有一个优先级同时操作系统不会告诉你你应该设置什么优先级以及不应该设置什么优先级。然而,如果你将要做的事情有不良后果它会给你警告;但是选择权还是在你手里。
在上面的截图中,你可以看到其中的一个优先级前有一个标识。这个标志表示进程当前的优先级。当你设置一个进程的优先级时,你只是设置一个应用程序的实例。这意味着同一个应用程序当前正在运行的实例仍然是默认优先级。近一步,一个程序任何新的实例都会按照默认优先级运行。
作者:DanielWise
出处:http://www.cnblogs.com/danielWise/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。