转自:http://www.cnblogs.com/lmule/archive/2010/08/18/1802774.htmlhtml
简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程当中拥有独立的内存单元,而多个线程共享内存,从而极大地提升了程序的运行效率。
线程在执行过程当中与进程仍是有区别的。每一个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。可是线程不可以独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分能够同时执行。但操做系统并无将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。算法
进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源.
一个线程能够建立和撤销另外一个线程;同一个进程中的多个线程之间能够并发执行.数据库
进程和线程的主要差异在于它们是不一样的操做系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程 产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,因此多进 程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享某些变量的并发操做,只能用线程,不能 用进程。若是有兴趣深刻的话,我建议大家看看《现代操做系统》或者《操做系统的设计与实现》。对就个问题说得比较清楚。安全
进程(process)是一块包含了某些资源的内存区域。操做系统利用进程把它的工做划分为一些功能单元。服务器
进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。多线程
当运行.NET程序时,进程还会把被称为CLR的软件层包含到它的内存空间中。上一章曾经对CLR作了详细描述。该软件层是在进程建立期间由运行时宿主载入的(参见4.2.3节)。架构
线程只能归属于一个进程而且它只能访问该进程所拥有的资源。当操做系统建立一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。主线程将执行运行时宿主, 而运行时宿主会负责载入CLR。并发
应用程序(application)是由一个或多个相互协做的进程组成的。例如,Visual Studio开发环境就是利用一个进程编辑源文件,并利用另外一个进程完成编译工做的应用程序。app
在Windows NT/2000/XP操做系统下,咱们能够经过任务管理器在任意时间查看全部的应用程序和进程。尽管只打开了几个应用程序,可是一般状况下将有大约30个进程同时运行。 事实上,为了管理当前的会话和任务栏以及其余一些任务,系统执行了大量的进程。函数
在运行于32位处理器上的32位Windows操做系统中,可将一个进程视为一段大小为4GB(232字节)的线性内存空间,它起始于0x00000000结束于0xFFFFFFFF。这段内存空间不能被其余进程所访问,因此称为该进程的私有空间。这段空间被平分为两块,2GB被系统全部,剩下2GB被用户全部。
若是有N个进程运行在同一台机器上,那么将须要N×4GB的海量RAM,还好事实并不是如此。
System.Diagnostics.Process类的实例能够引用一个进程,被引用的进程包含如下几种。
经过该类所包含的方法和字段,能够建立或销毁一个进程,而且能够得到一个进程的相关信息。下面将讨论一些使用该类实现的常见任务。
5.2.3 建立和销毁子进程
下面的程序建立了一个称为子进程的新进程。在这种状况下,初始的进程称为父进程。子进程启动了一个记事本应用程序。父进程的线程在等待1秒后销毁该子进程。该程序的执行效果就是打开并关闭记事本。
例5-1
静态方法Start()可使用已存在的Windows文件扩展名关联机制。例如,咱们能够利用下面的代码执行一样的操做。
默认状况下,子进程将继承其父进程的安全上下文。但还可使用Process.Start()方法的一个重载版本在任意用户的安全上下文中启动该子 进程,固然须要经过一个System.Diagnostics. ProcessStartInfo类的实例来提供该用户的用户名和密码。
有些应用程序须要这种功能。实际上,一般来讲在同一台机器上同时运行一个应用程序的多个实例并无意义。
直到如今,为了在Windows下知足上述约束,开发者最经常使用的方法仍然是使用有名互斥体(named mutex)技术(参见5.7.2节)。然而采用这种技术来知足上述约束存在如下缺点:
幸而在System.Diagnostics.Process类中拥有GetCurrentProcess()(返回当前进程)和GetPro- cesses()(返回机器上全部的进程)这样的静态方法。在下面的程序中咱们为上述问题找到了一个优雅且简单的解决方案。
例5-2
经过方法参数指定了远程机器的名字后,GetProcesses()方法也能够返回远程机器上全部的进程。
能够调用System.Environment类中的静态方法Exit(int exitCode)或FailFast(stringmessage)终止当前进程。Exit()方法是最好的选择,它将完全终止进程并向操做系统返回指 定的退出代码值。之因此称为完全终止是由于当前对象的全部清理工做以及finally块的执行都将由不一样的线程完成。固然,终止进程将花费必定的时间。
顾名思义,FailFast()方法能够迅速终止进程。Exit()方法所作的预防措施将被它忽略。只有一个包含了指定信息的严重错误会被操做系统记录到日志中。你可能想要在探查问题的时候使用该方法,由于能够将该程序的完全终止视为数据恶化的原由。
一个线程包含如下内容。
全部这些元素都归于线程执行上下文的名下。处在同一个进程中的全部线程均可以访问该进程所包含的地址空间,固然也包含存储在该空间中的全部资源。
咱们不许备讨论线程在内核模式或者用户模式执行的问题。尽管.NET之前的Windows一直使用这两种模式,而且依然存在,可是对.NET Framework来讲它们是不可见的。
并行使用一些线程一般是咱们在实现算法时的天然反应。实际上,一个算法每每由一系列能够并发执行的任务组成。可是须要引发注意的是,使用大量的线程将引发过多的上下文切换,最终反而影响了性能。
一样,几年前咱们就注意到,预测每18个月处理器运算速度增长一倍的摩尔定律已再也不成立。处理器的频率停滞在3GHz~4GHz上下。这是因为物理 上的限制,须要一段时间才能取得突破。同时,为了在性能竞争中不会落败,较大的处理器制造商如AMD和Intel目前都将目标转向多核芯片。所以咱们能够 预计在接下去的几年中这种类型的架构将普遍被采用。在这种状况下,改进应用性能的惟一方案就是合理地利用多线程技术。
必需要了解,执行.NET应用的线程实际上仍然是Windows线程。可是,当某个线程被CLR所知时,咱们将它称为受托管的线程。具体来讲,由受 托管的代码建立出来的线程就是受托管的线程。若是一个线程由非托管的代码所建立,那么它就是非托管的线程。不过,一旦该线程执行了受托管的代码它就变成了 受托管的线程。
一个受托管的线程和非托管的线程的区别在于,CLR将建立一个System.Threading.Thread类的实例来表明并操做前者。在内部实现中,CLR将一个包含了全部受托管线程的列表保存在一个叫作ThreadStore地方。
CLR确保每个受托管的线程在任意时刻都在一个AppDomain中执行,可是这并不表明一个线程将永远处在一个AppDomain中,它能够随着时间的推移转到其余的AppDomain中。关于AppDomain的概念参见4.1。
从安全的角度来看,一个受托管的线程的主用户与底层的非托管线程中的Windows主用户是无关的。
咱们能够问本身下面这个问题: 个人计算机只有一个处理器,然而在任务管理器中咱们却能够看到数以百计的线程正同时运行在机器上!这怎么可能呢?
多亏了抢占式多任务处理,经过它对线程的调度,使得上述问题成为可能。调度器做为Windows内核的一部分,将时间切片,分红一段段的时间片。这 些时间间隔以毫秒为精度且长度并不固定。针对每一个处理器,每一个时间片仅服务于单独一个线程。线程的迅速执行给咱们形成了它们在同时运行的假象。咱们在两个 时间片的间隔中进行上下文切换。该方法的优势在于,那些正在等待某些Windows资源的线程将不会浪费时间片,直到资源有效为止。
之因此用抢占式这个形容词来修饰这种多任务管理方式,是由于在此种方式下线程将被系统强制性中断。那些对此比较好奇的人应该了解到,在上下文切换的 过程当中,操做系统会在下一个线程将要执行的代码中插入一条跳转到下一个上下文切换的指令。该指令是一个软中断,若是线程在遇到这条指令前就终止了(例如, 它正在等待某个资源),那么该指定将被删除而上下文切换也将提早发生。
抢占式多任务处理的主要缺点在于,必须使用一种同步机制来保护资源以免它们被无序访问。除此以外,还有另外一种多任务管理模型,被称为协调式多任务 管理,其中线程间的切换将由线程本身负责完成。该模型广泛认为太过危险,缘由在于线程间的切换不发生的风险太大。如咱们在4.2.8节中所解释的那样,该 机制会在内部使用以提高某些服务器的性能,例如SQL Server2005。但Windows操做系统仅仅实现了抢占式多任务处理。
某些任务拥有比其余任务更高的优先级,它们须要操做系统为它们申请更多的处理时间。例如,某些由主处理器负责的外围驱动器必须不能被中断。另外一类高优先级的任务就是图形用户界面。事实上,用户不喜欢等待用户界面被重绘。
那些从Win32世界来的用户知道在CLR的底层,也就是Windows操做系统中,能够为每一个线程赋予一个0~31的优先级。但你没法在.NET的世界中也使用这些数值,由于:
1. 进程的优先级
可使用Process类中的类型为ProcessPriorityClass的PriorityClass{get;set;}属性为进程赋予一个优先级。System.Diagnostics.ProcessPriorityClass枚举包含如下值:
若是某个进程中属于Process类的PriorityBoostEnabled属性的值为true(默认值为true),那么当该进程占据前台窗口的时候,它的优先级将增长一个单位。只有当Process类的实例引用的是本机进程时,才可以访问该属性。
能够经过如下操做利用任务管理器来改变一个进程的优先级:在所选的进程上点击右键>设置优先级>从提供的6个值(和上图所述一致)中作出选择。
Windows操做系统有一个优先级为0的空闲进程。该进程不能被其余任何进程使用。根据定义,进程的活跃度用时间的百分比表示为:100%减去在空闲进程中所耗费时间的比率。
2. 线程的优先级
每一个线程能够结合它所属进程的优先级,并使用System.Threading.Thread类中类型为ThreadPriority的 Priority{get;set;}属性定义各自的优先级。System.Threading.Thread- Priority包含如下枚举值:
在大多数应用程序中,不须要修改进程和线程的优先级,它们的默认值为Normal。
CLR会自动将一个System.Threading.Thread类的实例与各个受托管的线程关联起来。可使用该对象从线程自身或从其余线程来 操纵线程。还能够经过System.Threading.Thread类的静态属性CurrentThread来得到当前线程的对象。
Thread类有一个功能使咱们可以很方便的调试多线程应用程序,该功能容许咱们使用一个字符串为线程命名:
只需经过建立一个Thread类的实例,就能够在当前的进程中建立一个新的线程。该类拥有多个构造函数,它们将接受一个类型为 System.Threading.ThreadStart或System.Threading.Parame-trizedThreadStart的委 托对象做为参数,线程被建立出来后首先执行该委托对象所引用的方法。使用ParametrizedThreadStart类型的委托对象容许用户为新线程 将要执行的方法传入一个对象做为参数。Thread类的一些构造函数还接受一个整型参数用于设置线程要使用的最大栈的大小,该值至少为128KB(即 131072字节)。建立了Thread类型的实例后,必须调用Thread.Start()方法以真正启动这个线程。
例5-3
该程序输出:
在这个例子中,咱们使用Join()方法挂起当前线程,直到调用Join()方法的线程执行完毕。该方法还存在包含参数的重载版本,其中的参数用于 指定等待线程结束的最长时间(即超时)所花费的毫秒数。若是线程中的工做在规定的超时时段内结束,该版本的Join()方法将返回一个布尔量True。
可使用Thread类的Sleep()方法将一个正在执行的线程挂起一段特定的时间,还能够经过一个以毫秒为单位的整型值或者一个 System.TimeSpan结构的实例设定这段挂起的时间。该结构的一个实例能够设定一个精度为1/10 ms(100ns)的时间段,可是Sleep()方法的最高精度只有1ms。
咱们也能够从将要挂起的线程自身或者另外一个线程中使用Thread类的Suspend()方法将一个线程的活动挂起。在这两种状况中,线程都将被阻 塞直到另外一个线程调用了Resume()方法。相对于Sleep()方法,Suspend()方法不会当即将线程挂起,而是在线程到达下一个安全点之 后,CLR才会将该线程挂起。安全点的概念参见4.7.11节。
5.3.8 终止一个线程
一个线程能够在如下场景中将本身终止。
第一种状况不过重要,咱们将主要关注另两种状况。在这两种状况中,均可以使用Abort()方法(经过当前线程或从当前线程以外的一个线程)。使用 该方法将在线程中引起一个类型为ThreadAbortException的异常。因为线程正处于一种被称为AbortRequested的特殊状态,该 异常具备一个特殊之处:当它被异常处理所捕获后,将自动被从新抛出。只有在异常处理中调用Thread.ResetAbort()这个静态方法(若是咱们 有足够的权限)才能阻止它的传播。
例5-4 主线程的自杀
当线程A对线程B调用了Abort()方法,建议调用B的Join()方法,让A一直等待直到B终止。Interrupt()方法也能够将一个处于 阻塞状态的线程(即因为调用了Wait()、Sleep()或者Join()其中一个方法而阻塞)终止。该方法会根据要被终止的线程是否处于阻塞状态而表 现出不一样的行为。
Thread类提供了IsBackground{get;set}的布尔属性。当前台线程还在运行时,它会阻止进程被终止。另外一方面,一旦所指的进 程中再也不有前台线程,后台线程就会被CLR自动终止(调用Abort()方法)。IsBackground的默认值为false,这意味着全部的线程默认 状况处于前台状态。
Thread类拥有一个System.Threading.ThreadState枚举类型的字段ThreadState,它包含如下枚举值:
有关每一个状态的具体描述能够在MSDN上一篇名为“ThreadStateEnumeration”的文章中找到。该枚举类型是一个二进制位域,这 表示一个该类型的实例能够同时表示多个枚举值。例如,一个线程能够同时处于Running、AbortRequested和Background这三种状 态。二进制位域的概念参见10.11.3节。
根据咱们在前面的章节中所了解的知识,咱们定义了如图5-1所示的简化的状态图。
图5-1 简化的托管线程状态图
在多线程应用(一个或多个处理器)的计算中会使用到同步这个词。实际上,这些应用程序的特色就是它们拥有多个执行单元,而这些单元在访问资源的时候 可能会发生冲突。线程间会共享同步对象,而同步对象的目的在于可以阻塞一个或多个线程,直到另外一个线程使得某个特定条件获得知足。
咱们将看到,存在多种同步类与同步机制,每种制针对一个或一些特定的需求。若是要利用同步构建一个复杂的多线程应用程序,那么颇有必要先掌握本章的内容。咱们将在下面的内容中尽力区分他们,尤为要指出那些在各个机制间最微妙的区别。
合理地同步一个程序是最精细的软件开发任务之一,单这一个主题就足以写几本书。在深刻到细节以前,应该首先确认使用同步是否不可避免。一般,使用一些简单的规则可让咱们远离同步问题。在这些规则中有线程与资源的亲缘性规则,咱们将在稍后介绍。
应该意识到,对程序中资源的访问进行同步时,其难点来自因而使用细粒度锁仍是粗粒度锁这个两难的选择。若是在访问资源时采用粗粒度的同步方式,虽然 能够简化代码可是也会把本身暴露在争用瓶颈的问题上。若是粒度过细,代码又会变的很复杂,以致于维护工做使人生厌。而后又会赶上死锁和竞态条件这些在下面 章节将要介绍的问题。
所以在咱们开始谈论有关同步机制以前,有必要先了解一下有关竞态条件和死锁的概念。
5.4.1 竞态条件
竞态条件指的是一种特殊的状况,在这种状况下各个执行单元以一种没有逻辑的顺序执行动做,从而致使意想不到的结果。
举一个例子,线程T修改资源R后,释放了它对R的写访问权,以后又从新夺回R的读访问权再使用它,并觉得它的状态仍然保持在它释放它以后的状态。可是在写访问权释放后到从新夺回读访问权的这段时间间隔中,可能另外一个线程已经修改了R的状态。
另外一个经典的竞态条件的例子就是生产者/消费者模型。生产者一般使用同一个物理内存空间保存被生产的信息。通常说来,咱们不会忘记在生产者与消费者 的并发访问之间保护这个空间。容易被咱们忘记的是生产者必须确保在生产新信息前,旧的信息已被消费者所读取。若是咱们没有采起相应的预防措施,咱们将面临 生产的信息从未被消费的危险。
若是静态条件没有被妥善的管理,将致使安全系统的漏洞。同一个应用程序的另外一个实例极可能会引起一系列开发者所预计不到的事件。通常来讲,必须对那 种用于确认身份鉴别结果的布尔量的写访问作最完善的保护。若是没有这么作,那么在它的状态被身份鉴别机制设置后,到它被读取以保护对资源的访问的这段时间 内,颇有可能已经被修改了。已知的安全漏洞不少都归咎于对静态条件不恰当的管理。其中之一甚至影响了Unix操做系统的内核。
死锁指的是因为两个或多个执行单元之间相互等待对方结束而引发阻塞的状况。例如:
一个线程T1得到了对资源R1的访问权。
一个线程T2得到了对资源R2的访问权。
T1请求对R2的访问权可是因为此权力被T2所占而不得不等待。
T2请求对R1的访问权可是因为此权力被T1所占而不得不等待。
T1和T2将永远维持等待状态,此时咱们陷入了死锁的处境!这种问题比你所遇到的大多数的bug都要隐秘,针对此问题主要有三种解决方案:
前两种技术效率更高可是也更加难于实现。事实上,它们都须要很强的约束,而这点随着应用程序的演变将愈来愈难以维护。尽管如此,使用这些技术不会存在失败的状况。
大的项目一般使用第三种方法。事实上,若是项目很大,通常来讲它会使用大量的资源。在这种状况下,资源之间发生冲突的几率很低,也就意味着失败的状况会比较罕见。咱们认为这是一种乐观的方法。秉着一样的精神,咱们在19.5节描述了一种乐观的数据库访问模型。
volatile字段能够被多个线程访问。咱们假设这些访问没有作任何同步。在这种状况下,CLR中一些用于管理代码和内存的内部机制将负责同步工 做,可是此时不能确保对该字段读访问总能读取到最新的值,而声明为volatile的字段则能提供这样的保证。在C#中,若是一个字段在它的声明前使用了 volatile关键字,则该字段被声明为volatile。
不是全部的字段均可以成为volatile,成为这种类型的字段有一个条件。若是一个字段要成为volatile,它的类型必须是如下所列的类型中的一种:
你可能已经注意到了,只有值或者引用的位数不超过本机整型值的位数(4或8由底层处理器决定)的类型才能成为volatile。这意味着对更大的值类型进行并发访问必须进行同步,下面咱们将会对此进行讨论。
经验显示,那些须要在多线程状况下被保护的资源一般是整型值,而这些被共享的整型值最多见的操做就是增长/减小以及相 加。.NETFramework利用System.Threading.Interlocked类提供了一个专门的机制用于完成这些特定的操做。这个类提 供了Increment()、Decrement()与Add()三个静态方法,分别用于对int或者long类型变量的递增、递减与相加操做,这些变量 以引用方式做为参数传入。咱们认为使用Interlocked类让这些操做具备了原子性。
下面的程序显示了两个线程如何并发访问一个名为counter的整型变量。一个线程将其递增5次,另外一个将其递减5次。
例5-5
该程序输出(以非肯定方式输出,意味着每执行一次显示的结果都是不一样的):
若是咱们不让这些线程在每次修改变量后休眠10毫秒,那么它们将有足够的时间在一个时间片中完成它们的任务,那样也就不会出现交叉操做,更不用说并发访问了。
Interlocked类还容许使用Exchange()静态方法,以原子操做的形式交换某些变量的状态。还可使用CompareExchange()静态方法在知足一个特定条件的基础上以原子操做的形式交换两个值。
以原子操做的方式完成简单的操做无疑是很重要的,可是这还远不能涵盖全部须要用到同步的事例。System.Threading.Monitor类几乎容许将任意一段代码设置为在某个时间仅能被一个线程执行。咱们将这段代码称之为临界区。
Monitor类提供了Enter(object)与Exit(object)这两个静态方法。这两个方法以一个对象做为参数,该对象提供了一个简 单的方式用于惟一标识那个将以同步方式访问的资源。当一个线程调用了Enter()方法,它将等待以得到访问该引用对象的独占权(仅当另外一个线程拥有该权 力的时候它才会等待)。一旦该权力被得到并使用,线程能够对同一个对象调用Exit()方法以释放该权力。
一个线程能够对同一个对象屡次调用Enter(),只要对同一对象调用相同次数的Exit()来释放独占访问权。
一个线程也能够在同一时间拥有多个对象的独占权,可是这样会产生死锁的状况。
毫不能对一个值类型的实例调用Enter()与Exit()方法。
无论发生了什么,必须在finally子句中调用Exit()以释放全部的独占访问权。
若是在例5-5中,一个线程非要将counter作一次平方而另外一个线程非要将counter乘2,咱们就不得不用Monitor类去替换对Interlocked类的使用。f1()与f2()的代码将变成下面这样:
例5-6[1]
人们很容易想到用counter来代替typeof(Program),可是counter是一个值类型的静态成员。须要注意平方和倍增操做是不知足交换律的,因此counter的最终结果是非肯定性的。
C#语言经过lock关键字提供了一种比使用Enter()和Exit()方法更加简洁的选择。咱们的程序能够改写为下面这个样子:
例5-7
和for以及if关键字同样,若是被lock关键字定义的块仅包含一条指令,就再也不须要花括号。咱们能够再次改写为:
使用lock关键字将引导C#编译器建立出相应的try/finally块,这样仍旧能够预期到任何可能引起的异常。可使用Reflector或者ildasm.exe工具验证这一点。
和前面的例子同样,咱们一般在一个静态方法中使用Monitor类配合一个Type类的实例。一样,咱们每每会在一个非静态方法中使用this关键 字来实现同步。在两种状况下,咱们都是经过一个在类外部可见的对象对自身进行同步。若是其余部分的代码也利用这些对象来实现自身的同步,就会出现问题。为 了避免这种潜在的问题,咱们推荐使用一个类型为object的名为SyncRoot的私有成员,至于该成员是静态的仍是非静态的则由须要而定。
例5-8
System.Collections.ICollection接口提供了object类型的SyncRoot{get;}属性。大多数的集合类 (泛型或非泛型)都实现了该接口。一样地,可使用该属性同步对集合中元素的访问。不过在这里SyncRoot模式并无被真正的应用,由于咱们对访问进 行同步所使用对象不是私有的。
例5-9
若一个类的每一个实例在同一时间不能被一个以上的线程所访问,则该类称之为一个线程安全的类。为了建立一个线程安全的类,只需将咱们见过的 SyncRoot模式应用于它所包含的方法。若是一个类想变成线程安全的,而又不想为类中代码增长过多负担,那么有一个好方法就是像下面这样为其提供一个 通过线程安全包装的继承类。
例5-10
另外一种方法就是使用System.Runtime.Remoting.Contexts.SynchronizationAttribute,这点咱们将在本章稍后讨论。
该方法与Enter()类似,只不过它是非阻塞的。若是资源的独占访问权已经被另外一个线程占据,该方法将当即返回一个false返回值。咱们也能够 调用TryEnter()方法,让它以毫秒为单位阻塞一段有限的时间。由于该方法的返回结果并不肯定,而且当得到独占访问权后必须在finally子句中 释放该权力,因此建议当TryEnter()失败时当即退出正在调用的函数:
例5-11[2]
Wait()、Pulse()与PulseAll()方法必须在一块儿使用而且须要结合一个小场景才能被正确理解。咱们的想法是这样的:一个线程得到 了某个对象的独占访问权,而它决定等待(经过调用Wait())直到该对象的状态发生变化。为此,该线程必须暂时失去对象独占访问权,以便让另外一个线程修 改对象的状态。修改对象状态的线程必须使用Pulse()方法通知那个等待线程修改完成。下面有一个小场景具体说明了这一状况。
若是Wait(OBJ)被一个调用了屡次Enter(OBJ)的线程所调用,那么该线程将须要调用相同次数的Exit(OBJ)以释放对OBJ的访问权。即便在这种状况下,另外一个线程调用一次Pulse(OBJ)就足以将第一个线程变成非阻塞态。
下面的程序经过ping与pong两个线程以交替的方式使用一个ball对象的访问权来演示该功能。
例5-12
该程序输出(以不肯定的方式):
pong线程没有结束而且仍然阻塞在Wait()方法上。因为pong线程是第二个得到ball对象的独占访问权的,因此才致使了该结果。