计算机组成原理7

死锁

死锁指的是由于两个或多个执行单元之间相互等待对方结束而引起阻塞的情况。例如:

一个线程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类

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

下面的程序显示了两个线程如何并发访问一个名为counter的整型变量。一个线程将其递增5次,另一个将其递减5次。

例5-5

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

如果我们不让这些线程在每次修改变量后休眠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()方法以释放该权力。

一个线程可以对同一个对象多次调用Enter(),只要对同一对象调用相同次数的Exit()来释放独占访问权。

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

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

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

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

例5-6[1]

人们很容易想到用counter来代替typeof(Program),但是counter是一个值类型的静态成员。需要注意平方和倍增操作是不满足交换律的,所以counter的最终结果是非确定性的。

5.6.2 C#的lock关键字

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

例5-7

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

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

5.6.3 SyncRoot模式

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

例5-8

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

例5-9

5.6.4 线程安全类

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

例5-10

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

5.6.5 Monitor.TryEnter()方法

该方法与Enter()相似,只不过它是非阻塞的。如果资源的独占访问权已经被另一个线程占据,该方法将立即返回一个false返回值。我们也可以调用TryEnter()方法,让它以毫秒为单位阻塞一段有限的时间。因为该方法的返回结果并不确定,并且当获得独占访问权后必须在finally子句中释放该权力,所以建议当TryEnter()失败时立即退出正在调用的函数:

例5-11[2]

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

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()的顺序到达非阻塞态。

如果Wait(OBJ)被一个调用了多次Enter(OBJ)的线程所调用,那么该线程将需要调用相同次数的Exit(OBJ)以释放对OBJ的访问权。即使在这种情况下,另一个线程调用一次Pulse(OBJ)就足以将第一个线程变成非阻塞态。

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

例5-12

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

pong线程没有结束并且仍然阻塞在Wait()方法上。由于pong线程是第二个获得ball对象的独占访问权的,所以才导致了该结果。

转载于:https://my.oschina.net/u/2557747/blog/547393

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值