有资源网

搜索
有资源网 首页 编程语言 查看内容

进程与线程的区别

2019-7-26 01:07| 发布者: admin| 查看: 194| 评论: 0

摘要: 简而言之,一个程序至少有一个历程,一个历程至少有一个线程. 线程的分别尺度小于历程,使得多线程程序的并发性高。别的,历程在实行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地进步了程序的运行效率。

简而言之,一个程序至少有一个历程,一个历程至少有一个线程.
线程的分别尺度小于历程,使得多线程程序的并发性高。
别的,历程在实行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地进步了程序的运行效率。
线程在实行过程中与历程照旧有区别的。每个独立的线程有一个程序运行的入口、次序实行序列和程序的出口。但是线程不能够独立实行,必须依存在应用程序中,由应用程序提供多个线程实行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个实行部门可以同时实行。但操作系统并没有将多个线程看做多个独立的应用,来实现历程的调理和管理以及资源分配。这就是历程和线程的重要区别。

历程是具有肯定独立功能的程序关于某个数据聚集上的一次运行活动,历程是系统举行资源分配和调理的一个独立单元.
线程是历程的一个实体,是CPU调理和分派的基本单元,它是比历程更小的能独立运行的基本单元.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个历程的其他的线程共享历程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个历程中的多个线程之间可以并发实行.

历程和线程的主要差别在于它们是不同的操作系统资源管理方式。历程有独立的地址空间,一个历程瓦解后,在掩护模式下不会对其它历程产生影响,而线程只是一个历程中的不同实行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就便是整个历程死掉,所以多历程的程序要比多线程的程序结实,但在历程切换时,泯灭资源较大,效率要差一些。但对于一些要求同时举行并且又要共享某些变量的并发操作,只能用线程,不能用历程。如果有爱好深入的话,我发起你们看看《现代操作系统》大概《操作系统的计划与实现》。对就个问题说得比较清晰。

5.1 简介

历程(process)是一块包罗了某些资源的内存区域。操作系统使用历程把它的工作分别为一些功能单元。

历程中所包罗的一个或多个实行单元称为线程(thread)。历程还拥有一个私有的虚拟地址空间,该空间仅能被它所包罗的线程访问。

当运行.NET程序时,历程还会把被称为CLR的软件层包罗到它的内存空间中。上一章曾经对CLR做了具体描述。该软件层是在历程创建期间由运行时宿主载入的(拜见4.2.3节)。

线程只能归属于一个历程并且它只能访问该历程所拥有的资源。当操作系统创建一个历程后,该历程会主动申请一个名为主线程或首要线程的线程。主线程将实行运行时宿主, 而运行时宿主会负责载入CLR。

应用程序(application)是由一个或多个相互协作的历程组成的。例如,Visual Studio开发情况就是使用一个历程编辑源文件,并使用另一个历程完成编译工作的应用程序。

在Windows NT/2000/XP操作系统下,我们可以通过任务管理器在任意时间查看所有的应用程序和历程。只管只打开了几个应用程序,但是通常情况下将有约莫30个历程同时运行。 事实上,为了管理当前的会话和任务栏以及其他一些任务,系统实行了大量的历程。

5.2 历程

5.2.1 简介

在运行于32位处置惩罚器上的32位Windows操作系统中,可将一个历程视为一段大小为4GB(232字节)的线性内存空间,它起始于0x00000000竣事于0xFFFFFFFF。这段内存空间不能被其他历程所访问,所以称为该历程的私有空间。这段空间被中分为两块,2GB被系统所有,剩下2GB被用户所有。

如果有N个历程运行在同一台呆板上,那么将需要N×4GB的海量RAM,还功德实并非如此。

  • Windows是按需为每个历程分配内存的,4GB是32位系统中一个历程所占空间的上限。
  • 将历程所需的内存分别为4KB大小的内存页,并根据使用情况将这些内存页存储在硬盘上或加载到RAM中,通过系统的这种虚拟内存机制,我们可以有效地镌汰对实际内存的需求量。当然这些对用户和开发者来说都是透明的。

5.2.2 System.Diagnostics.Process类

System.Diagnostics.Process类的实例可以引用一个历程,被引用的历程包罗以下几种。

  • 该实例的当前历程。
  • 本机上除了当前历程的其他历程。
  • 远程呆板上的某个历程。

通过该类所包罗的方法和字段,可以创建或烧毁一个历程,并且可以得到一个历程的相干信息。下面将讨论一些使用该类实现的常见任务。

5.2.3 创建和烧毁子历程

下面的程序创建了一个称为子历程的新历程。在这种情况下,初始的历程称为父历程。子历程启动了一个记事本应用程序。父历程的线程在等待1秒后烧毁该子历程。该程序的实行结果就是打开并关闭记事本。

例5-1

编程语言-进程与线程的区别(1)

静态方法Start()可以使用已存在的Windows文件扩展名关联机制。例如,我们可以使用下面的代码实行同样的操作。

编程语言-进程与线程的区别(2)

默认情况下,子历程将继承其父历程的安全上下文。但还可以使用Process.Start()方法的一个重载版本在任意用户的安全上下文中启动该子历程,当然需要通过一个System.Diagnostics. ProcessStartInfo类的实例来提供该用户的用户名和暗码。

5.2.4 避免在一台呆板上同时运行同一应用程序的多个实例

有些应用程序需要这种功能。实际上,通常来说在同一台呆板上同时运行一个应用程序的多个实例并没故意义。

直到如今,为了在Windows下满意上述束缚,开发者最常用的方法仍旧是使用著名互斥体(named mutex)技术(拜见5.7.2节)。然而采用这种技术来满意上述束缚存在以下缺点:

  • 该技术具有使互斥体的名字被其他应用程序所使用的较小的、潜在的风险。在这种情况下该技术将不再有效并且会造成很难检测到的bug。
  • 该技术不能办理我们仅答应一个应用程序产生N个实例这种一般的问题。

幸而在System.Diagnostics.Process类中拥有GetCurrentProcess()(返回当前历程)和GetPro- cesses()(返回呆板上所有的历程)如许的静态方法。在下面的程序中我们为上述问题找到了一个优雅且简朴的办理方案。

例5-2

编程语言-进程与线程的区别(3)

通过方法参数指定了远程呆板的名字后,GetProcesses()方法也可以返回远程呆板上所有的历程。

5.2.5 停止当前历程

可以调用System.Environment类中的静态方法Exit(int exitCode)或FailFast(stringmessage)停止当前历程。Exit()方法是最好的选择,它将彻底停止历程并向操作系统返回指定的退出代码值。之所以称为彻底停止是由于当前对象的所有清算工作以及finally块的实行都将由不同的线程完成。当然,停止历程将耗费肯定的时间。

顾名思义,FailFast()方法可以迅速停止历程。Exit()方法所做的预防步调将被它忽略。只有一个包罗了指定信息的严重错误会被操作系统纪录到日志中。你大概想要在探盘问题的时候使用该方法,由于可以将该程序的彻底停止视为数据恶化的起因。

5.3 线程

5.3.1 简介

一个线程包罗以下内容。

  • 一个指向当前被实行指令的指令指针;
  • 一个栈;
  • 一个寄存器值的聚集,界说了一部门描述正在实行线程的处置惩罚器状态的值;
  • 一个私有的数据区。

所有这些元素都归于线程实行上下文的名下。处在同一个历程中的所有线程都可以访问该历程所包罗的地址空间,当然也包罗存储在该空间中的所有资源。

我们禁绝备讨论线程在内核模式大概用户模式实行的问题。只管.NET从前的Windows不停使用这两种模式,并且依然存在,但是对.NET Framework来说它们是不可见的。

并利用用一些线程通常是我们在实现算法时的自然反应。实际上,一个算法往往由一系列可以并发实行的任务组成。但是需要引起留意的是,使用大量的线程将引起过多的上下文切换,终极反而影响了性能。

同样,几年前我们就留意到,猜测每18个月处置惩罚器运算速率增加一倍的摩尔定律已不再建立。处置惩罚器的频率停滞在3GHz~4GHz上下。这是由于物理上的限制,需要一段时间才气取得突破。同时,为了在性能竞争中不会落败,较大的处置惩罚器制造商如AMD和Intel目前都将目的转向多核芯片。因此我们可以预计在接下去的几年中这种范例的架构将广泛被采用。在这种情况下,改进应用性能的唯一方案就是合理地使用多线程技术。

5.3.2 受托管的线程与 Windows线程

必须要相识,实行.NET应用的线程实际上仍旧是Windows线程。但是,当某个线程被CLR所知时,我们将它称为受托管的线程。具体来说,由受托管的代码创建出来的线程就是受托管的线程。如果一个线程由非托管的代码所创建,那么它就黑白托管的线程。不外,一旦该线程实行了受托管的代码它就酿成了受托管的线程。

一个受托管的线程和非托管的线程的区别在于,CLR将创建一个System.Threading.Thread类的实例来代表并操作前者。在内部实现中,CLR将一个包罗了所有受托管线程的列表生存在一个叫做ThreadStore地方。

CLR确保每一个受托管的线程在任意时候都在一个AppDomain中实行,但是这并不代表一个线程将永久处在一个AppDomain中,它可以随着时间的推移转到其他的AppDomain中。关于AppDomain的概念拜见4.1。

从安全的角度来看,一个受托管的线程的主用户与底层的非托管线程中的Windows主用户是无关的。

5.3.3 抢占式多任务处置惩罚

我们可以问自己下面这个问题: 我的计算机只有一个处置惩罚器,然而在任务管理器中我们却可以看到数以百计的线程正同时运行在呆板上!这怎么大概呢?

多亏了抢占式多任务处置惩罚,通过它对线程的调理,使得上述问题成为大概。调理器作为Windows内核的一部门,将时间切片,分成一段段的时间片。这些时间间隔以毫秒为精度且长度并不固定。针对每个处置惩罚器,每个时间片仅服务于单独一个线程。线程的迅速实行给我们造成了它们在同时运行的假象。我们在两个时间片的间隔中举行上下文切换。该方法的优点在于,那些正在等待某些Windows资源的线程将不会浪费时间片,直到资源有效为止。

之所以用抢占式这个形容词来修饰这种多任务管理方式,是由于在此种方式下线程将被系统逼迫性中断。那些对此比较好奇的人应该相识到,在上下文切换的过程中,操作系统会在下一个线程将要实行的代码中插入一条跳转到下一个上下文切换的指令。该指令是一个软中断,如果线程在碰到这条指令前就停止了(例如,它正在等待某个资源),那么该指定将被删除而上下文切换也将提前发生。

抢占式多任务处置惩罚的主要缺点在于,必须使用一种同步机制来掩护资源以避免它们被无序访问。除此之外,还有另一种多任务管理模子,被称为协调式多任务管理,其中线程间的切换将由线程自己负责完成。该模子普遍以为太过伤害,缘故原由在于线程间的切换不发生的风险太大。如我们在4.2.8节中所解释的那样,该机制会在内部使用以提升某些服务器的性能,例如SQL Server2005。但Windows操作系统仅仅实现了抢占式多任务处置惩罚。

5.3.4 历程与线程的优先级

某些任务拥有比其他任务更高的优先级,它们需要操作系统为它们申请更多的处置惩罚时间。例如,某些由主处置惩罚器负责的外围驱动器必须不能被中断。另一类高优先级的任务就是图形用户界面。事实上,用户不喜欢等待用户界面被重绘。

那些从Win32天下来的用户知道在CLR的底层,也就是Windows操作系统中,可以为每个线程赋予一个0~31的优先级。但你无法在.NET的天下中也使用这些数值,由于:

  • 它们无法描述自身的寄义。
  • 随着时间的流逝这些值黑白常容易变化的。

1. 历程的优先级

可以使用Process类中的范例为ProcessPriorityClass的PriorityClass{get;set;}属性为历程赋予一个优先级。System.Diagnostics.ProcessPriorityClass罗列包罗以下值:

编程语言-进程与线程的区别(4)

如果某个历程中属于Process类的PriorityBoostEnabled属性的值为true(默认值为true),那么当该历程占据前台窗口的时候,它的优先级将增加一个单元。只有当Process类的实例引用的是本机历程时,才气够访问该属性。

可以通过以下操作使用任务管理器来改变一个历程的优先级:在所选的历程上点击右键>设置优先级>从提供的6个值(和上图所述一致)中做出选择。

Windows操作系统有一个优先级为0的空闲历程。该历程不能被其他任何历程使用。根据界说,历程的活泼度用时间的百分比体现为:100%减去在空闲历程中所泯灭时间的比率。

2. 线程的优先级

每个线程可以联合它所属历程的优先级,并使用System.Threading.Thread类中范例为ThreadPriority的Priority{get;set;}属性界说各自的优先级。System.Threading.Thread- Priority包罗以下罗列值:

编程语言-进程与线程的区别(5)

在大多数应用程序中,不需要修改历程和线程的优先级,它们的默认值为Normal。

5.3.5 System.Threading.Thread类

CLR会主动将一个System.Threading.Thread类的实例与各个受托管的线程关联起来。可以使用该对象从线程自身或从其他线程来操纵线程。还可以通过System.Threading.Thread类的静态属性CurrentThread来获恰当火线程的对象。

编程语言-进程与线程的区别(6)

Thread类有一个功能使我们能够很方便的调试多线程应用程序,该功能答应我们使用一个字符串为线程定名:

编程语言-进程与线程的区别(7)

5.3.6 创建与Join一个线程

只需通过创建一个Thread类的实例,就可以在当前的历程中创建一个新的线程。该类拥有多个构造函数,它们将接受一个范例为System.Threading.ThreadStart或System.Threading.Parame-trizedThreadStart的委托对象作为参数,线程被创建出来后起首实行该委托对象所引用的方法。使用ParametrizedThreadStart范例的委托对象答应用户为新线程将要实行的方法传入一个对象作为参数。Thread类的一些构造函数还接受一个整型参数用于设置线程要使用的最大栈的大小,该值至少为128KB(即131072字节)。创建了Thread范例的实例后,必须调用Thread.Start()方法以真正启动这个线程。

例5-3

编程语言-进程与线程的区别(8)

该程序输出:

编程语言-进程与线程的区别(9)

在这个例子中,我们使用Join()方法挂起当火线程,直到调用Join()方法的线程实行完毕。该方法还存在包罗参数的重载版本,其中的参数用于指定等待线程竣事的最长时间(即超时)所耗费的毫秒数。如果线程中的工作在规定的超时时段内竣事,该版本的Join()方法将返回一个布尔量True。

5.3.7 挂起一个线程

可以使用Thread类的Sleep()方法将一个正在实行的线程挂起一段特定的时间,还可以通过一个以毫秒为单元的整型值大概一个System.TimeSpan布局的实例设定这段挂起的时间。该布局的一个实例可以设定一个精度为1/10 ms(100ns)的时间段,但是Sleep()方法的最高精度只有1ms。

编程语言-进程与线程的区别(10)

我们也可以从将要挂起的线程自身大概另一个线程中使用Thread类的Suspend()方法将一个线程的活动挂起。在这两种情况中,线程都将被壅闭直到另一个线程调用了Resume()方法。相对于Sleep()方法,Suspend()方法不会立即将线程挂起,而是在线程到达下一个安全点之后,CLR才会将该线程挂起。安全点的概念拜见4.7.11节。

5.3.8 停止一个线程

一个线程可以在以下场景中将自己停止。

  • 从自己开始实行的方法(主线程中的Main()方法,其他线程中ThreadStart委托对象所引用的方法)中退出。
  • 被自己停止。
  • 被另一个线程停止。

第一种情况不太重要,我们将主要关注另两种情况。在这两种情况中,都可以使用Abort()方法(通过当火线程或从当火线程之外的一个线程)。使用该方法将在线程中引发一个范例为ThreadAbortException的异常。由于线程正处于一种被称为AbortRequested的特殊状态,该异常具有一个特殊之处:当它被异常处置惩罚所捕捉后,将主动被重新抛出。只有在异常处置惩罚中调用Thread.ResetAbort()这个静态方法(如果我们有充足的权限)才气制止它的传播。

例5-4 主线程的自尽

编程语言-进程与线程的区别(11)

当线程A对线程B调用了Abort()方法,发起调用B的Join()方法,让A不停等待直到B停止。Interrupt()方法也可以将一个处于壅闭状态的线程(即由于调用了Wait()、Sleep()大概Join()其中一个方法而壅闭)停止。该方法会根据要被停止的线程是否处于壅闭状态而体现出不同的行为。

  • 如果该方法被另一个线程调用时,要被停止的线程处于壅闭状态,那么会产生ThreadInterruptedException异常。
  • 如果该方法被另一个线程调用时,要被停止的线程不处于壅闭状态,那么一旦该线程进入壅闭状态,就会引发异常。这种行为与线程对自己调用Interrupt()方法是一样的。

5.3.9 前台线程与后台线程

Thread类提供了IsBackground{get;set}的布尔属性。当前台线程还在运行时,它会制止历程被停止。另一方面,一旦所指的历程中不再有前台线程,后台线程就会被CLR主动停止(调用Abort()方法)。IsBackground的默认值为false,这意味着所有的线程默认情况处于前台状态。

5.3.10 受托管线程的状态图

Thread类拥有一个System.Threading.ThreadState罗列范例的字段ThreadState,它包罗以下罗列值:

编程语言-进程与线程的区别(12)

有关每个状态的具体描述可以在MSDN上一篇名为“ThreadStateEnumeration”的文章中找到。该罗列范例是一个二进制位域,这体现一个该范例的实例可以同时体现多个罗列值。例如,一个线程可以同时处于Running、AbortRequested和Background这三种状态。二进制位域的概念拜见10.11.3节。

根据我们在前面的章节中所相识的知识,我们界说了如图5-1所示的简化的状态图。

编程语言-进程与线程的区别(13)

图5-1 简化的托管线程状态图

5.4 访问资源同步简介

在多线程应用(一个或多个处置惩罚器)的计算中会使用到同步这个词。实际上,这些应用程序的特点就是它们拥有多个实行单元,而这些单元在访问资源的时候大概会发生冲突。线程间会共享同步对象,而同步对象的目的在于能够壅闭一个或多个线程,直到另一个线程使得某个特定条件得到满意。

我们将看到,存在多种同步类与同步机制,每种制针对一个或一些特定的需求。如果要使用同步构建一个复杂的多线程应用程序,那么很有必要先把握本章的内容。我们将在下面的内容中尽力区分他们,尤其要指出那些在各个机制间最玄妙的区别。

合理地同步一个程序是最精细的软件开发任务之一,单这一个主题就足以写几本书。在深入到细节之前,应该起首确认使用同步是否不可避免。通常,使用一些简朴的规则可以让我们远离同步问题。在这些规则中有线程与资源的亲缘性规则,我们将在稍后先容。

应该意识到,对程序中资源的访问举行同步时,其难点来自于是使用细粒度锁照旧粗粒度锁这个两难的选择。如果在访问资源时采用粗粒度的同步方式,虽然可以简化代码但是也会把自己袒露在争用瓶颈的问题上。如果粒度过细,代码又会变的很复杂,以至于维护工作令人生厌。然后又会遇上死锁和竞态条件这些在下面章节将要先容的问题。

因此在我们开始批评有关同步机制之前,有必要先相识一下有关竞态条件和死锁的概念。

5.4.1 竞态条件

竞态条件指的是一种特殊的情况,在这种情况下各个实行单元以一种没有逻辑的次序实行动作,从而导致意想不到的结果。

举一个例子,线程T修改资源R后,释放了它对R的写访问权,之后又重新夺回R的读访问权再使用它,并以为它的状态仍旧保持在它释放它之后的状态。但是在写访问权释放后到重新夺回读访问权的这段时间间隔中,大概另一个线程已经修改了R的状态。

另一个经典的竞态条件的例子就是生产者/消费者模子。生产者通常使用同一个物理内存空间生存被生产的信息。一般说来,我们不会忘记在生产者与消费者的并发访问之间掩护这个空间。容易被我们忘记的是生产者必须确保在生产新信息前,旧的信息已被消费者所读取。如果我们没有采取相应的预防步调,我们将面临生产的信息从未被消费的伤害。

如果静态条件没有被妥善的管理,将导致安全系统的毛病。同一个应用程序的另一个实例很大概会引发一系列开发者所预计不到的事件。一般来说,必须对那种用于确认身份辨别结果的布尔量的写访问做最完满的掩护。如果没有这么做,那么在它的状态被身份辨别机制设置后,到它被读取以掩护对资源的访问的这段时间内,很有大概已经被修改了。已知的安全毛病很多都归咎于对静态条件不恰当的管理。其中之一甚至影响了Unix操作系统的内核。

5.4.2 死锁

死锁指的是由于两个或多个实行单元之间相互等待对方竣事而引起壅闭的情况。例如:

一个线程T1得到了对资源R1的访问权。

一个线程T2得到了对资源R2的访问权。

T1哀求对R2的访问权但是由于此权利被T2所占而不得不等待。

T2哀求对R1的访问权但是由于此权利被T1所占而不得不等待。

T1和T2将永久维持等待状态,此时我们陷入了死锁的处境!这种问题比你所碰到的大多数的bug都要机密,针对此问题主要有三种办理方案:

  • 在同一时候不答应一个线程访问多个资源。
  • 为资源访问权的获取界说一个关系次序。换句话说,当一个线程已经得到了R1的访问权后,将无法得到R2的访问权。当然,访问权的释放必须遵循相反的次序。
  • 为所有访问资源的哀求系统地界说一个最大等待时间(超时时间),并妥善处置惩罚哀求失败的情况。几乎所有的.NET的同步机制都提供了这个功能。

前两种技术效率更高但是也更加难于实现。事实上,它们都需要很强的束缚,而这点随着应用程序的演变将越来越难以维护。只管如此,使用这些技术不会存在失败的情况。

大的项目通常使用第三种方法。事实上,如果项目很大,一般来说它会使用大量的资源。在这种情况下,资源之间发生冲突的概率很低,也就意味着失败的情况会比较罕见。我们以为这是一种乐观的方法。秉着同样的精力,我们在19.5节描述了一种乐观的数据库访问模子。

5.5 使用volatile字段与Interlocked类实现同步

5.5.1 volatile字段

volatile字段可以被多个线程访问。我们假设这些访问没有做任何同步。在这种情况下,CLR中一些用于管理代码和内存的内部机制将负责同步工作,但是此时不能确保对该字段读访问总能读取到最新的值,而声明为volatile的字段则能提供如许的包管。在C#中,如果一个字段在它的声明前使用了volatile关键字,则该字段被声明为volatile。

不是所有的字段都可以成为volatile,成为这种范例的字段有一个条件。如果一个字段要成为volatile,它的范例必须是以下所列的范例中的一种:

  • 引用范例(这里只有访问该范例的引用是同步的,访问其成员并不同步)。
  • 一个指针(在不安全的代码块中)。
  • sbyte、byte、short、ushort、int、uint、char、float、bool(工作在64位处置惩罚器上时为double、long与ulong)。
  • 一个使用以下底层范例的罗列范例:byte、sbyte、short、ushort、int、uint(工作在64位的处置惩罚器上时为double、long与ulong)。

你大概已经留意到了,只有值大概引用的位数不超过本机整型值的位数(4或8由底层处置惩罚器决定)的范例才气成为volatile。这意味着对更大的值范例举行并发访问必须举行同步,下面我们将会对此举行讨论。

5.5.2 System.Threading.Interlocked类

经验体现,那些需要在多线程情况下被掩护的资源通常是整型值,而这些被共享的整型值最常见的操作就是增加/镌汰以及相加。.NETFramework使用System.Threading.Interlocked类提供了一个专门的机制用于完成这些特定的操作。这个类提供了Increment()、Decrement()与Add()三个静态方法,分别用于对int大概long范例变量的递增、递减与相加操作,这些变量以引用方式作为参数传入。我们以为使用Interlocked类让这些操作具有了原子性。

下面的程序体现了两个线程怎样并发访问一个名为counter的整型变量。一个线程将其递增5次,另一个将其递减5次。

例5-5

编程语言-进程与线程的区别(14)

该程序输出(以非确定方式输出,意味着每实行一次体现的结果都是不同的):

编程语言-进程与线程的区别(15)

编程语言-进程与线程的区别(16)

如果我们不让这些线程在每次修改变量后休眠10毫秒,那么它们将有充足的时间在一个时间片中完成它们的任务,那样也就不会出现交织操作,更不用说并发访问了。

5.5.3 Interlocked类提供的其他功能

Interlocked类还答应使用Exchange()静态方法,以原子操作的形式互换某些变量的状态。还可以使用CompareExchange()静态方法在满意一个特定条件的基础上以原子操作的形式互换两个值。

5.6 使用System.Threading.Monitor类与C#的lock关键字实现同步

以原子操作的方式完成简朴的操作无疑是很重要的,但是这还远不能涵盖所有需要用到同步的事例。System.Threading.Monitor类几乎答应将任意一段代码设置为在某个时间仅能被一个线程实行。我们将这段代码称之为临界区。

5.6.1 Enter()方法和Exit()方法

Monitor类提供了Enter(object)与Exit(object)这两个静态方法。这两个方法以一个对象作为参数,该对象提供了一个简朴的方式用于唯一标识谁人将以同步方式访问的资源。当一个线程调用了Enter()方法,它将等待以得到访问该引用对象的独占权(仅当另一个线程拥有该权利的时候它才会等待)。一旦该权利被得到并使用,线程可以对同一个对象调用Exit()方法以释放该权利。

编程语言-进程与线程的区别(17)一个线程可以对同一个对象多次调用Enter(),只要对同一对象调用雷同次数的Exit()来释放独占访问权。

一个线程也可以在同一时间拥有多个对象的独占权,但是如许会产生死锁的情况。

绝不能对一个值范例的实例调用Enter()与Exit()方法。

不管发生了什么,必须在finally子句中调用Exit()以释放所有的独占访问权。

如果在例5-5中,一个线程非要将counter做一次平方而另一个线程非要将counter乘2,我们就不得不用Monitor类去更换对Interlocked类的使用。f1()与f2()的代码将酿成下面如许:

例5-6[1]

编程语言-进程与线程的区别(18)编程语言-进程与线程的区别(19)

编程语言-进程与线程的区别(20)

人们很容易想到用counter来取代typeof(Program),但是counter是一个值范例的静态成员。需要留意平方和倍增操作是不满意互换律的,所以counter的最闭幕果黑白确定性的。

5.6.2 C#的lock关键字

C#语言通过lock关键字提供了一种比使用Enter()和Exit()方法更加简洁的选择。我们的程序可以改写为下面这个样子:

例5-7

编程语言-进程与线程的区别(21)

和for以及if关键字一样,如果被lock关键字界说的块仅包罗一条指令,就不再需要花括号。我们可以再次改写为:

编程语言-进程与线程的区别(22)

使用lock关键字将引导C#编译器创建出相应的try/finally块,如许仍旧可以预期到任何大概引发的异常。可以使用Reflector大概ildasm.exe工具验证这一点。

5.6.3 SyncRoot模式

和前面的例子一样,我们通常在一个静态方法中使用Monitor类配合一个Type类的实例。同样,我们往往会在一个非静态方法中使用this关键字来实现同步。在两种情况下,我们都是通过一个在类外部可见的对象对自身举行同步。如果其他部门的代码也使用这些对象来实现自身的同步,就会出现问题。为了避免这种潜在的问题,我们推荐使用一个范例为object的名为SyncRoot的私有成员,至于该成员是静态的还黑白静态的则由需要而定。

例5-8

编程语言-进程与线程的区别(23)

System.Collections.ICollection接口提供了object范例的SyncRoot{get;}属性。大多数的聚集类(泛型或非泛型)都实现了该接口。同样地,可以使用该属性同步对聚会集元素的访问。不外在这里SyncRoot模式并没有被真正的应用,由于我们对访问举行同步所使用对象不是私有的。

例5-9

编程语言-进程与线程的区别(24)

5.6.4 线程安全类

若一个类的每个实例在同一时间不能被一个以上的线程所访问,则该类称之为一个线程安全的类。为了创建一个线程安全的类,只需将我们见过的SyncRoot模式应用于它所包罗的方法。如果一个类想酿成线程安全的,而又不想为类中代码增加过多负担,那么有一个好方法就是像下面如许为其提供一个颠末线程安全包装的继承类。

例5-10

编程语言-进程与线程的区别(25)

编程语言-进程与线程的区别(26)

另一种方法就是使用System.Runtime.Remoting.Contexts.SynchronizationAttribute,这点我们将在本章稍后讨论。

5.6.5 Monitor.TryEnter()方法

编程语言-进程与线程的区别(27)

该方法与Enter()相似,只不外它黑白壅闭的。如果资源的独占访问权已经被另一个线程占据,该方法将立即返回一个false返回值。我们也可以调用TryEnter()方法,让它以毫秒为单元壅闭一段有限的时间。由于该方法的返回结果并不确定,并且当得到独占访问权后必须在finally子句中释放该权利,所以发起当TryEnter()失败时立即退出正在调用的函数:

例5-11[2]

编程语言-进程与线程的区别(28)

5.6.6 Monitor类的Wait()方法, Pulse()方法以及PulseAll()方法

编程语言-进程与线程的区别(29)

编程语言-进程与线程的区别(30)

Wait()、Pulse()与PulseAll()方法必须在一起使用并且需要联合一个小场景才气被精确明确。我们的想法是如许的:一个线程得到了某个对象的独占访问权,而它决定等待(通过调用Wait())直到该对象的状态发生变化。为此,该线程必须临时失去对象独占访问权,以便让另一个线程修改对象的状态。修改对象状态的线程必须使用Pulse()方法通知谁人等待线程修改完成。下面有一个小场景具体说明了这一情况。

  • 拥有OBJ对象独占访问权的T1线程,调用Wait(OBJ)方法将它自己注册到OBJ对象的被动等待列表中。
  • 由于以上的调用,T1失去了对OBJ的独占访问权。因此,另一个线程T2通过调用Enter(OBJ)得到OBJ的独占访问权。
  • T2终极修改了OBJ的状态并调用Pulse(OBJ)通知了这次修改。该调用将导致OBJ被动等待列表中的第一个线程(在这里是T1)被移到OBJ的主动等待列表的首位。而一旦OBJ的独占访问权被释放,OBJ主动等待列表中的第一个线程将被确保得到该权利。然后它就从Wait(OBJ)方法中退出等待状态。
  • 在我们的场景中,T2调用Exit(OBJ)以释放对OBJ的独占访问权,接着T1恢复访问权并从Wait(OBJ)方法中退出。
  • PulseAll()将使得被动等待列表中的线程全部转移到主动等待列表中。留意这些线程将按照它们调用Wait()的次序到达非壅闭态。

编程语言-进程与线程的区别(31)如果Wait(OBJ)被一个调用了多次Enter(OBJ)的线程所调用,那么该线程将需要调用雷同次数的Exit(OBJ)以释放对OBJ的访问权。即使在这种情况下,另一个线程调用一次Pulse(OBJ)就足以将第一个线程酿成非壅闭态。

下面的程序通过ping与pong两个线程以瓜代的方式使用一个ball对象的访问权来演示该功能。

例5-12

编程语言-进程与线程的区别(32)

编程语言-进程与线程的区别(33)

该程序输出(以不确定的方式):

编程语言-进程与线程的区别(34)

pong线程没有竣事并且仍旧壅闭在Wait()方法上。由于pong线程是第二个得到ball对象的独占访问权的,所以才导致了该结果。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

鲜花

握手

雷人

路过

鸡蛋

最新评论

返回顶部