计算机的“线程”和“进程”是两个相关的概念。二者都表示计算机按特定顺序执行的指令序列集合。线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。不同线程或进程中的指令可以并行执行。
进程是为了提高 CPU 的执行效率,减少程序等待带来的 CPU 空转以及其他计算机软硬件资源的浪费而提出来的概念,是按照既定的顺序进行的程序执行和资源分配,从而完成某项用户任务的执行过程或活动,进程在执行过程中拥有独立的内存单元和初始入口点,并且进程存活过程中始终拥有独立的内存地址空间。
线程则是为了减少进程切换和创建的开销,提高执行效率和节省资源,而引入的概念,是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源,从而提高程序的运行效率。
线程使复杂的应用程序能够更有效地利用 CPU,即使在只有一个 CPU 的计算机上也是如此。如果只有一个 CPU,则每次只能执行一个线程。如果一个线程执行不使用 CPU 的长时间运行的操作(如磁盘读/写操作),则第一个操作完成之前可以执行另一个线程。通过在其他线程等待操作完成的同时执行线程,应用程序可以最大限度地利用 CPU。对于大量占用磁盘 I/O 的多用户应用程序(如数据库服务器),这尤其有效。具有多个微处理器 (CPU) 的计算机可以同时在每个 CPU 上执行一个线程。例如,如果某计算机有八个 CPU,则它可以同时执行八个线程。
一般意义上的Windows 服务、应用程序多数都是以单独进程的方式运行。线程则存在于进程内,通常被称为被称为是轻权进程或者轻量级进程,最常见的多线程程序如复制文件的进度显示进程。进度条显示窗体运行于进程的主线程,用于处理进度的显示和用户的交互(如鼠标点击和移动窗体等操作);复制文件的任务则运行于系统后台线程,进行复制文件数量、大小、复制时间的计算以及实际的文件复制动作等的处理。
操作系统执行应用程序实例时,它将创建一个单元(称为进程)来管理该实例。此进程包含一个执行线程。它是由应用程序代码执行的一系列编程指令。例如,如果一个简单应用程序具有一组可串行执行的指令,则整个应用程序只有一个执行路径或线程。更复杂的应用程序可能有几个任务,这些任务可以一前一后地执行,而不是串行执行。通过为每个任务启动独立的进程,应用程序可以实现串行操作。但是,启动进程会消耗大量资源。而应用程序可启动独立的线程。相对来说,这将消耗较少的资源。而且,可以独立于与某进程关联的其他线程来安排每个线程的执行。
多个进程的存在使得计算机能够一次执行多个任务。而多个线程的存在使得进程能够分解工作以便并行执行。具有多个处理器、多核处理器或超线程进程的计算机可以同时运行多个线程。并行处理多个线程可以极大地提高程序性能。但并不是所有的都能够完全并行处理。有时候必须要同步线程。一个线程可能必须等待另一个线程的结果,或者一个线程可能需要独占访问另一个线程正在使用的资源。同步问题是多线程应用程序中出现 bug 的一个常见原因。有时候线程可能最终等待的是永远不会变得可用的资源。必须使用某种形式的互斥以确保一次仅有一个线程访问资源。如果互斥执行不正确,则可能形成死锁条件,这种条件下,任何线程都无法执行。
进程中的每个线程都独立运行。除非使这些线程相互可见,否则线程分别执行,对进程中的其他线程一无所知。线程共享公共资源,但是,必须使用信号灯或其他进程间的通信方法协调它们的工作。
操作系统使用进程将它们正在执行的不同应用程序分开。线程是操作系统分配处理器时间的基本单元,并且进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。
.NET Framework 将操作系统进程进一步细分为由 System..::.AppDomain 表示的、称为应用程序域的轻量托管子进程。一个或多个托管线程(由 System.Threading..::.Thread 表示)可以在同一个托管进程中的一个或任意数目的应用程序域中运行。虽然每个应用程序域都是用单个线程启动的,但该应用程序域中的代码可以创建附加应用程序域和附加线程。其结果是托管线程可以在同一个非托管进程中的应用程序域之间自由移动;您可能只有一个线程在若干应用程序域之间移动。
支持抢先多任务处理的操作系统可以创建多个进程中的多个线程同时执行的效果。它通过以下方式实现这一点:在需要处理器时间的线程之间分割可用处理器时间,并轮流为每个线程分配处理器时间片。当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文。
时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎也是在同时执行。这实际上就是多处理器系统中发生的情形,在此类系统中,可执行线程分布在多个可用处理器中。
何时使用多个线程
需要用户交互的软件必须尽可能快地对用户的活动作出反应,以便提供丰富多彩的用户体验。但同时它必须执行必要的计算以便尽可能快地将数据呈现给用户。如果应用程序仅使用一个执行线程,则可以将异步编程与 .NET Framework 远程处理或使用 ASP.NET 创建的 XML Web services 结合使用,在使用自己的计算机的处理时间之外还使用其他计算机的处理时间,从而提高对用户的响应速度并减少应用程序的数据处理时间。如果您正在进行大量的输入/输出工作,则还可以使用 I/O 完成端口来提高应用程序的响应速度。
多个线程的优点
无论如何,要提高对用户的响应速度并且处理所需数据以便几乎同时完成工作,使用多个线程是一种最为强大的技术。在具有一个处理器的计算机上,多个线程可以通过利用用户事件之间很小的时间段在后台处理数据来达到这种效果。例如,在另一个线程正在重新计算同一应用程序中的电子表格的其他部分时,用户可以编辑该电子表格。
无需修改,同一个应用程序在具有多个处理器的计算机上运行时将极大地满足用户的需要。单个应用程序域可以使用多个线程来完成以下任务:
多个线程的缺点
建议您使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。线程处理还具有在设计应用程序时要考虑的资源要求和潜在冲突。这些资源要求如下所述:
-
系统将为进程、
AppDomain 对象和线程所需的上下文信息使用内存。因此,可以创建的进程、
AppDomain 对象和线程的数目会受到可用内存的限制。
-
跟踪大量的线程将占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。
-
使用许多线程控制代码执行非常复杂,并可能产生许多 bug。
-
销毁线程需要了解可能发生的问题并对那些问题进行处理。
提供对资源的共享访问会造成冲突。为了避免冲突,必须对共享资源进行同步或控制对共享资源的访问。如果在相同或不同的应用程序域中未能正确地使访问同步,则会导致出现一些问题,这些问题包括死锁和争用条件等,其中死锁是指两个线程都停止响应,并且都在等待对方完成;争用条件是指由于意外地出现对两个事件的执行时间的临界依赖性而发生反常的结果。系统提供了可用于协调多个线程之间的资源共享的同步对象。减少线程的数目使同步资源更为容易。
需要同步的资源包括:
线程处理与应用程序设计
一般情况下,要为不会阻止其他线程的相对较短的任务处理多个线程并且不需要对这些任务执行任何特定调度时,使用 ThreadPool 类是一种最简单的方式。但是,有多个理由创建您自己的线程:
-
如果您需要使一个任务具有特定的优先级。
-
如果您具有可能会长时间运行(并因此阻止其他任务)的任务。
-
如果您需要将线程放置到单线程单元中(所有
ThreadPool 线程均处于多线程单元中)。
-
如果您需要与该线程关联的稳定标识。例如,您应使用一个专用线程来中止该线程,将其挂起或按名称发现它。
-
如果您需要运行与用户界面交互的后台线程,.NET Framework 2.0 版提供了 BackgroundWorker 组件,该组件可以使用事件与用户界面线程的跨线程封送进行通信。
线程处理和异常
在线程中执行异常处理。线程(甚至是后台线程)中的未处理异常通常会终止进程。以下为此规则的三种例外情况: