[VB.NET]多线程——高级同步技术

多线程应用程序通常使用等待句柄和监视器对象来同步多个线程。下表介绍了可用于同步线程的部分 .NET 框架类。

类 用途
AutoResetEvent 等待句柄,用于通知一个或多个等待线程发生了一个事件。AutoResetEvent 在等待线程被释放后自动将状态更改为已发出信号。
Interlocked 为多个线程共享的变量提供原子操作。
ManualResetEvent 等待句柄,用于通知一个或多个等待线程发生了一个事件。手动重置事件的状态将保持为已发出信号,直至 Reset 方法将其设置为未发出信号状态。同样,该状态将保持为未发出信号,直至 Set 方法将其设置为已发出信号状态。当对象的状态为已发出信号时,任意数量的等待线程(即通过调用一个等待函数开始对指定事件对象执行等待操作的线程)都可以被释放。
Monitor 提供同步访问对象的机制。Visual Basic .NET 应用程序调用 SyncLock 以使用监视器对象。
Mutex 等待句柄,可用于进程间同步。
ReaderWriterLock 定义用于实现单个写入者和多个读取者的锁定。
Timer 提供按指定间隔运行任务的机制。
WaitHandle 封装操作系统特有的、等待对共享资源进行独占访问的对象。

等待句柄
等待句柄是将一个线程的状态通知另一个线程的对象。线程可以使用等待句柄,通知其他线程它们需要对资源进行独占访问。然后,其他线程必须等到没有线程在使用等待句柄时才能使用此资源。等待句柄有两种状态:已发出信号和未发出信号。不属于任何线程的等待句柄处于已发出信号状态。属于某线程的等待句柄处于未发出信号状态。

线程通过调用一种等待方法(例如 WaitOne、WaitAny 或 WaitAll)来请求等待句柄的所有权。等待方法是与单独线程的 Join 方法相类似的阻塞调用。

如果没有其他线程拥有该等待句柄,则调用将立即返回 True,等待句柄的状态将更改为未发出信号,而拥有等待句柄的线程将继续运行。
如果线程调用了等待句柄的一种等待方法,但该等待句柄归另一线程所有,则调用线程将等待指定的时间(如果指定了超时),或者无限期地等待(未指定超时),直至其他线程释放等待句柄。如果指定了超时,并且在超时到期前释放等待句柄,则调用返回 True。否则,调用返回 False,并且进行调用的线程将继续运行。
拥有等待句柄的线程在运行结束后,或不再需要等待句柄时将调用 Set 方法。其他线程通过调用 Reset 方法,或者调用 WaitOne、WaitAll 或 WaitAny 以及成功地等待某一线程调用 Set 方法之后,可以将等待句柄的状态重置为未发出信号。在单个等待线程被释放后,系统将 AutoResetEvent 句柄自动重置为未发出信号。如果没有线程处于等待状态,则事件对象的状态将保持为已发出信号。

方法 用途
WaitOne 接受一个等待句柄作为参数,并使调用线程处于等待状态,直至另一个进程调用 Set 将当前的等待句柄设置为已发出信号。
WaitAny 接受一个等待句柄数组作为参数,并使调用线程处于等待状态,直至任一指定的等待句柄已通过调用 Set 设置为已发出信号。
WaitAll 接受一个等待句柄数组作为参数,并使调用线程处于等待状态,直至所有指定的等待句柄已通过调用 Set 设置为已发出信号。
Set 将指定的等待句柄的状态设置为已发出信号,并使任何等待线程继续运行。
Reset 将指定事件的状态设置为未发出信号。

Visual Basic .NET 常用的等待句柄有三种:互斥对象、ManualResetEvent 和 AutoResetEvent。后两种通常称为同步事件。

互斥对象
互斥对象是一次只能由一个线程拥有的同步对象。实际上,“互斥”这个名称来自互斥对象的所有权相互排斥这一事实。如果线程要对资源进行独占访问,则需要请求互斥对象的所有权。由于在任何时刻,只能有一个线程拥有互斥对象,因此其他线程必须等待,直至获得互斥对象的所有权后才能使用资源。

WaitOne 方法使调用线程等待获得互斥对象的所有权。如果拥有互斥对象的线程正常终止,则互斥对象的状态将设置为已发出信号,下一个等待线程将获得所有权。

同步事件
同步事件用于通知其他线程某件事情已发生或某个资源已可用。不要被这些使用“事件”一词的项误导。同步事件与其他 Visual Basic 事件不同,它们实际上是等待句柄。与其他等待句柄类似,同步事件也有两种状态:已发出信号和未发出信号。调用同步事件的一种等待方法的线程必须等待,直至另一个线程通过调用 Set 方法向事件发出通知。有两种同步事件类。线程使用 Set 方法将 ManualResetEvent 实例的状态设置为已发出信号。线程使用 Reset 方法,或者在控制返回到一个等待 WaitOne 的调用时,将 ManualResetEvent 实例的状态设置为未发出信号。还可以使用 Set 将 AutoResetEvent 类的实例设置为已发出信号,但是只要等待线程被通知事件已发出信号,这些实例就自动返回到未发出信号状态。

以下示例使用 AutoResetEvent 类来同步线程池任务。

Sub StartTest()
Dim AT As New AsyncTest()
AT.StartTask()
End Sub

Class AsyncTest
Private Shared AsyncOpDone As New _
System.Threading.AutoResetEvent(False)

Sub StartTask()
Dim Tpool As System.Threading.ThreadPool
Dim arg As String = "SomeArg"
Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _
AddressOf Task),arg) ' 将一个任务排队。
AsyncOpDone.WaitOne() ' 等待线程调用 Set。
MsgBox("线程运行结束。")
End Sub

Sub Task(ByVal Arg As Object)
MsgBox("线程正在启动。")
System.Threading.Thread.Sleep(4000) ' 等待 4 秒钟。
MsgBox("状态对象包含字符串 " & CStr(Arg))
AsyncOpDone.Set() ' 通知线程运行结束。
End Sub
End Class
监视器对象和 SyncLock
监视器对象用于确保代码块在运行时不会被其他线程运行的代码中断。换句话说,直到同步代码块中的代码运行结束后,其他线程中的代码才能运行。在 Visual Basic .NET 中,SyncLock 关键字用于简化对监视器对象的访问。在 Visual C#? .NET 中则使用 Lock 关键字。

例如,假设有一个反复异步读取数据并显示结果的程序。如果操作系统使用抢占式多任务处理技术,则可以中断正在运行的线程而将时间用于运行其他某个线程。如果不进行同步,则如果在显示数据时,代表数据的对象被其他线程修改,则可能会看到被部分更新的数据。SyncLock 语句可以保证代码段在运行时不会被中断。以下示例说明了如何使用 SyncLock 为显示过程提供数据对象的独占访问权限。

Class DataObject
Public ObjText As String
Public ObjTimeStamp As Date
End Class

Sub RunTasks()
Dim MyDataObject As New DataObject()
ReadDataAsync(MyDataObject)
SyncLock MyDataObject
DisplayResults(MyDataObject)
End SyncLock
End Sub

Sub ReadDataAsync(ByRef MyDataObject As DataObject)
' 添加代码以异步读取和处理数据。
End Sub

Sub DisplayResults(ByVal MyDataObject As DataObject)
' 添加代码以显示结果。
End Sub
如果需要确保代码段不会被在其它线程中运行的代码中断,请使用 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 锁定是独占锁定的一个很有用的替代选择,因为独占锁定使其他线程一直处于等待状态,即使那些线程并不需要更新数据。以下示例说明了如何使用 ReaderWriter 来协调多个线程的读写操作。

Class ReadWrite
' 可以从多个线程中安全地调用
' ReadData 和 WriteData 方法。
Public ReadWriteLock As New System.Threading.ReaderWriterLock()
Sub ReadData()
' 此过程从某个来源读取信息。
' 读取锁定禁止在线程完成读取之前写入数据,
' 同时允许其他线程调用 ReadData。
ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
Try
' 此处执行读取操作。
Finally
ReadWriteLock.ReleaseReaderLock() ' 释放读取锁定。
End Try
End Sub

Sub WriteData() ' 此过程将信息写入某个来源。 ' 写入锁定禁止在线程完成写入操作前 ' 读取或写入数据。 ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite) Try ' 此处执行写入操作。 Finally ReadWriteLock.ReleaseWriterLock() ' 释放写入锁定。 End Try End SubEnd Class死锁线程同步在多线程应用程序中十分重要,但在多个线程相互等待时总是存在死锁的危险。就象四个方向上都停有汽车的情况,每个人都在等待另一个人走,死锁使一切操作终止。显然,避免死锁非常重要。有许多情况会导致死锁,同样,避免死锁的方法也很多。虽然本文没有足够篇幅来讨论与死锁相关的所有问题,但有一点很重要,即认真规划是避免死锁的关键。在开始编码之前,通过图解多线程应用程序,通常可以预测死锁。

相关文章

Format[$] ( expr [ , fmt ] ) format 返回变体型 format$ 强...
VB6或者ASP 格式化时间为 MM/dd/yyyy 格式,竟然没有好的办...
在项目中添加如下代码:新建窗口来显示异常信息。 Namespace...
转了这一篇文章,原来一直想用C#做k3的插件开发,vb没有C#用...
Sub 分列() ‘以空格为分隔符,连续空格只算1个。对所选...
  窗体代码 1 Private Sub Text1_OLEDragDrop(Data As Dat...