高级同步技术
更新:2007 年 11 月
多线程应用程序经常使用等待句柄和监视器对象来同步多个线程。以下各节解释了在同步线程时如何使用下列 .NET Framework 类:AutoResetEvent、Interlocked、ManualResetEvent、Monitor、Mutex、ReaderWriterLock、Timer 和 WaitHandle。
等待句柄
等待句柄是将一个线程的状态通知给另一个线程的对象。线程可以使用等待句柄来通知其他线程它们需要独占访问某个资源。其他线程则必须等到等待句柄空闲时才能使用该资源。等待句柄有两个状态:signaled 和 nonsignaled。不属于任何线程的等待句柄处于 signaled 状态。属于某个线程的等待句柄处于 nonsignaled 状态。
线程通过调用等待方法之一(如 WaitOne、WaitAny 或 WaitAll)来请求对等待句柄的所属权。等待方法是与单个线程的 Join 方法类似的阻止调用。
如果其他线程都未拥有等待句柄,该调用将立即返回 True,等待句柄的状态更改为 nonsignaled,并且拥有等待句柄的线程将继续运行。
如果某个线程调用了等待句柄的一个等待方法,但等待句柄为另一个线程所有,则调用线程将等待一段指定的时间(如果指定了超时值),或者无限期地等下去(如果未指定超时值),直到另一个线程释放等待句柄为止。如果指定了超时值,而等待句柄在超时时间未到之前被释放,则该调用返回 True。否则,等待的调用将返回 False,且调用线程继续运行。
拥有等待句柄的线程在其完成后或不再需要等待句柄时调用 Set 方法。通过调用 Reset 方法,或者调用 WaitOne、WaitAny 或 WaitAll 并成功等待线程调用 Set,其他线程可以将等待句柄的状态重置为 nonsignaled。在释放单个等待线程之后,系统会自动将 AutoResetEvent 句柄重置为 nonsignaled。如果没有任何线程在等待,则该事件对象的状态将保持已发信号的状态。
有三种经常与 Visual Basic 一起使用的等待句柄:mutex 对象、ManualResetEvent 和 AutoResetEvent。后两个经常称作同步事件。
Mutex 对象
Mutex 对象是一次只能为一个线程所拥有的同步对象。实际上,之所以称为“mutex”,是因为对 mutex 对象的所属权是互相排斥的。线程需要独占访问某个资源时,将请求对 mutex 对象的所属权。由于一次只有一个线程可以拥有 mutex 对象,因此其他线程必须等到获得对 mutex 对象的所属权之后才能使用资源。
使用 WaitOne 方法可以让调用线程等待对 mutex 对象的所属权。如果某个线程在拥有 mutex 对象时正常终止,则该 mutex 对象的状态将被设置为 signaled,并且下一个等待线程将获得所属权。
同步事件
同步事件用于通知其他线程发生了某件事或某个资源可用。尽管该术语包含字样“事件”,但同步事件与其他 Visual Basic 事件不同:它们是真正的等待句柄。与其他等待句柄类似,同步事件也有两个状态:signaled 和 nonsignaled。
调用了同步事件的一个等待方法的线程必须等到另一个线程通过调用 Set 方法通知该事件。有两个同步事件类:ManualResetEvent 和 AutoResetEvent。
线程使用 Set 方法将 ManualResetEvent 实例的状态设置为 signaled。线程使用 Reset 方法将 ManualResetEvent 实例的状态设置为 nonsignaled,或者当控制权返回到等待调用 WaitOne 时,也将实例状态设置为 nonsignaled。
使用 Set 也可以将 AutoResetEvent 类的实例设置为 signaled,但在等待线程获悉该事件已变为 signaled 之后,这些实例立即自动返回 nonsignaled 状态。
监视器对象和 SyncLock
监视器对象用于确保代码块在运行时不会被其他线程上运行的代码中断。换言之,其他线程中的代码必须等到同步代码块中的代码运行结束后才能运行。
例如,假设有一个程序反复异步读取数据并显示结果。在使用抢占式多任务处理的操作系统上,运行中的线程可能会被操作系统中断,从而允许其他某个线程优先运行。在不同步的情况下,如果在显示数据时表示数据的对象被另一个线程修改,则会得到一个部分更新的数据视图。监视器对象可确保代码段在运行时不会被中断。Visual Basic 提供了 SyncLock 和 End SyncLock 语句以简化对监视器对象的访问。Visual C# 以相同的方式使用 Lock 关键字。
说明: |
---|
只有在同一对象实例上的 SyncLock 块中包含访问代码的情况下,才锁定对对象的访问。 |
有关 SyncLock 语句的信息,请参见 SyncLock 语句
Interlocked 类
可以使用 Interlocked 类的方法来避免在多个线程尝试同时更新或比较同一个值时可能出现的问题。使用这个类的方法可以安全地递增、递减、交换和比较任何线程中的值。下面的示例说明如何使用 Increment 方法来递增一个由在单独线程上运行的过程共享的变量。
Sub ThreadA(ByRef IntA As Integer)
System.Threading.Interlocked.Increment(IntA)
End Sub
Sub ThreadB(ByRef IntA As Integer)
System.Threading.Interlocked.Increment(IntA)
End Sub
ReaderWriter 锁
在某些情况下,可能希望只在写入数据时锁定资源,在不更新数据时允许多个客户端同时读取数据。ReaderWriterLock 类在线程修改资源时将强制其独占访问资源,但在读取资源时则允许非独占访问。ReaderWriter 锁可用于代替排它锁。使用排它锁时,即使其他线程不需要更新数据,也会让这些线程等待。
死锁
在多线程应用程序中,线程同步的价值是无法估量的,但始终存在产生 deadlock 的危险。一旦产生了死锁,将有多个线程互相等待,从而导致应用程序暂停。死锁类似于轿车在十字路口停车并且每个司机在等待其他司机先行的情况。避免死锁很重要,其关键在于仔细规划。在开始编码前绘制多线程应用程序关系图通常可以预测死锁情况。