VB.net学习笔记(二十七)线程同步上
Class AccountWrapper Private _a As Account Public Sub New(ByVal a As Account) _a = a End Sub Public Function Withdraw(ByVal amount As Double) As Boolean SyncLock Me '.... Return _a.Withdraw(amount) End SyncLock End Function End Class在这种方法中,Account对象不具有任何线程安全的特性,因为所有的线程安全都是由AccountWrapper类提供的。 采用线程安全包装器的方法,将开发线程安全的AccountWrapper类作为Account类的伸缩。 二、.NET对同步的支持 通知正在等待的线程已发生事件。 此类不能被继承。 ManualResetEvent 通知一个或多个正在等待的线程已发生事件。 此类不能被继承。 [SynchronizationAttribute] Public Class <span style="white-space:pre"> </span>Account Inherits ContextBoundObject <span style="white-space:pre"> </span>Sub ApprovedOrNotWithdraw (Amount) <span style="white-space:pre"> </span>1.Check the Account Balance <span style="white-space:pre"> </span>2.Update the Account with the new balance <span style="white-space:pre"> </span>3.Send approval to the ATM <span style="white-space:pre"> </span>End Sub End Class (二)同步代码区 第二种同步策略是有关特殊代码区的同步。如Monitor和 ReaderWriterLock 类、SyncLock 语句。 1.Monitor 类 锁即访问权限,获得锁即获得访问代码块(临界区)的权限。 通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还 可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。 使用Monitor.Enter()方法获得一个锁,然后,使用Monitor.Exit()方法释放该锁。一个线程获得锁,其他线程就要等到该锁被释放后才能使用。访问同步对象的线程可以采取的操作: Enter,TryEnter 获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令。 Wait 释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。 Pulse(信号),PulseAll 向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。 Exit 释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾。 ---------------------------------------------------------------------------- Monitor.Pulse()的意义?如果不调用它会造成怎样的后果?(http://zhidao.baidu.com/link?url=dbtqrVwQvGxcEKbPrCfhjzS__VA3wPcGr2nmAtMQMyngoAI-gm-AU7br0d4a-miDskzINbs5h2ERnOYpXQpEoK) 一、意义? 首先,同步的某一个对象包含若干引用,这些引用被处于三个状态的线程队列所占据,这三个队列分别是:拥有锁的线程(处于执行中)、就绪队列(马上就可以获得锁的)、等待队列(wait中即阻塞的,需要进入就绪队列才拿到锁)。 执行流向:等待队列---> 就绪队列---> 拥有锁的线程 当拥有锁的线程执行完毕让出了锁,就绪队列的线程才有机会一窝蜂上去抢,锁只有一个,抢不到的继续在就绪队列里等待下一次机会(当然也需要考虑优先级设置情况的,没有则是这样),如此,直到 就绪队列 里的线程全部执行完。 二、如果不调用它会造成怎样的后果? 1、 Infinite:无限期等待状态下,如果当前获得锁的线程不执行Pulse(),那么本线程一直处于阻塞状态,在等待队列中,得不到执行机会; 2、某一时长:则两个情况: 3、0:等待时长为零,则调用wait之后的线程直接进入就绪队列而不是 等待队列。
Imports System.Threading Namespace MonitorEnterExit Public Class EnterExit Private result As Integer = 0 Public Sub NonCriticalSection() '无临界区,乱序竞争 Console.WriteLine(("Entered Thread " & Thread.CurrentThread.GetHashCode().ToString)) For i As Integer = 1 To 5 Console.WriteLine(("Result = " & result & " ThreadID ” + Thread.CurrentThread.GetHashCode().ToString)) result += 1 Thread.Sleep(1000) Next i Console.WriteLine((”Exiting Thread " & Thread.CurrentThread.GetHashCode())) End Sub Public Sub CriticalSection() '有临界区,有Enter与Exit以便上锁独享 Monitor.Enter(Me) Console.WriteLine(("Entered Thread " & Thread.CurrentThread.GetHashCode.ToString)) For i As Integer = 1 To 5 Console.WriteLine(("Result = " & result & " ThreadID " + Thread.CurrentThread.GetHashCode.ToString)) result += 1 Thread.Sleep(1000) Next i Console.WriteLine(("Exiting Thread " & Thread.CurrentThread.GetHashCode())) Monitor.Exit(Me) End Sub Public Overloads Shared Sub Main(ByVal args() As [String]) Dim e As New EnterExit() If args.Length > 0 Then '带参时,无临界区,线程乱序竞争 Dim ntl As New Thread(New ThreadStart(AddressOf e.NonCriticalSection)) ntl.Start() Dim nt2 As New Thread(New ThreadStart(AddressOf e.NonCriticalSection)) nt2.Start() Else '不带参,有临界区,线程上锁独享 Dim ctl As New Thread(New ThreadStart(AddressOf e.CriticalSection)) ctl.Start() Dim ct2 As New Thread(New ThreadStart(AddressOf e.CriticalSection)) ct2.Start() End If Console.ReadLine() End Sub End Class End Namespace可以看到有临界区与无临界时结果有显著的区别: (1)Wait(等待)与Pulse(唤醒) 注意:只有在Enter()和Exit()代码块才可以调用Wait()和Pulse()方法. Imports System.Threading Namespace WaitAndPulse Public Class WaitPulsel '唤醒等待类1 Private result As Integer = 0 Private _IM As LockMe Public Sub New(ByVal lock As LockMe) _IM = lock End Sub Public Sub CriticalSection() Monitor.Enter(_IM) Console.WriteLine(("WaitPulsel: Entered Thread " & Thread.CurrentThread.GetHashCode.ToString)) For i As Integer = 1 To 5 Monitor.Wait(_IM) Console.WriteLine("WaitPulsel: WokeUp") Console.WriteLine(("WaitPulse 1: Result = ” + result.ToString + ” ThreadID " + Thread.CurrentThread.GetHashCode().ToString)) result += 1 Monitor.Pulse(_IM) Next i Console.WriteLine(("WaitPulsel: Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString)) Monitor.Exit(_IM) End Sub End Class Public Class WaitPulse2 '唤醒等待类2 Private result As Integer = 0 Friend _IM As LockMe Public Sub New(ByVal lock As LockMe) '3、接收lock到_IM _IM = lock End Sub Public Sub CriticalSection() Monitor.Enter(_IM) Console.WriteLine(("WaitPulse2: Entered Thread " & Thread.CurrentThread.GetHashCode().ToString)) For i As Integer = 1 To 5 Monitor.Pulse(_IM) Console.WriteLine("WaitPulse2: Result = " & result.ToString + " ThreadID ” & Thread.CurrentThread.GetHashCode.ToString) result += 1 Monitor.Wait(_IM) Console.WriteLine("WaitPulse2: WokeUp") Next i Console.WriteLine("WaitPulse2: Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString) Monitor.Exit(_IM) End Sub End Class Public Class ClassForMain '主程序类 Public Shared Sub Main() Dim lock As New LockMe() '1、lockme实例化 Dim e1 As New WaitPulsel(lock) '2、传送lock Dim e2 As New WaitPulse2(lock) Dim tl As New Thread(New ThreadStart(AddressOf e1.CriticalSection)) tl.Start() Dim t2 As New Thread(New ThreadStart(AddressOf e2.CriticalSection)) t2.Start() Console.ReadLine() End Sub End Class Public Class LockMe '被操作的类,此类的控制方式由WaitPulse1与WatiPulse2两个来决定 '空 End Class End Namespace结果如下: 说明:1处实例化被操作的类,2处再创建两个可以控制lock的方式方法,并将lock传递到_IM对象中。两条线程虽然各自按循环向前走,但是停还是走则由e1和e2中的wait与pulse来控制。具体的执行如下图的红线,最后线程1中在退出前,唤醒了线程2,所以e2临界区最后退出。 注意:Monitor 锁定对象(即引用类型),而不是值类型。 虽然可以将值类型传递到 Enter 和 Exit,但对每个调用它都分别进行了装箱。 由于每次调用都将创建一个单独的对象,所以绝不会阻止 Enter 并且它应该保护的代码不会真正同步。 此外,传递到 Exit 的对象不同于传递到 Enter 的对象,因此 Monitor 将引发 SynchronizationLockException 异常并显示消息“从代码的非同步块调用了对象同步方法。” 装箱与拆箱(左图): 值类型在栈上;引用类型(如对象)在堆上,它的地址是放在栈。装箱就将值类型(i)打包放在堆上从而使其由值类型变成引用类型(对象A),它的引用地址放在栈上obj_i。拆箱就是把引用类型(对象A)再次变回值类型i。 为什么Monitor 只能锁定对象(即引用类型)? Moniter锁定的是堆上的引用对象(右图,obj),如果变成了值类型,每次进入,就会装值类型装箱打包成引用类型,而每次装箱尽管里面内容一样,但形成地址不一样(第一次打包是A,第二次打包是B),循环进入的次数越多,就还会形成C、D、E……那么到底是上锁保护A还是上锁保护B呢?于是编译器就纳闷了,会抛出异常。 (2)TryEnter()方法 试图在一个对象上获得独占锁。如果当前线程获取该锁,则为 true;否则为 false。 Imports System.Threading Namespace MonitorTryEnter Public Class ATryEnter Public Sub New() End Sub Public Sub CriticalSection() Dim b As Boolean = Monitor.TryEnter(Me,1000) Console.WriteLine("Thread " & Thread.CurrentThread.GetHashCode & " TryEnter Value: " & b) If b Then '1、if For i As Integer = 1 To 3 Thread.Sleep(3000) Console.WriteLine(i & " " & Thread.CurrentThread.GetHashCode.ToString) Next i Monitor.Exit(Me) '2、释放锁 End If '1、endif End Sub Public Shared Sub Main() Dim a As New ATryEnter() Dim tl As New Thread(New ThreadStart(AddressOf a.CriticalSection)) Dim t2 As New Thread(New ThreadStart(AddressOf a.CriticalSection)) tl.Start() t2.Start() Console.ReadLine() End Sub End Class End Namespace说明:在1处无if时,结果是左图;无if时结果是右图。左图因为无if,最后异常,因为不上锁是也执行,最后释放锁时前面根本就没有上锁,还释放什么锁呢?于是抛出异常了。正常的做法是右图,用TryEnter去尝试,返回为真,才执行进而临界区的代码。 注意:正常的做法是1、为了保证线程进入临界区达到上锁目的,应对TryEnter进行判断;2、如果发生异常,应在Try…Catch…中的Finally块中放置Exit,以确保线程不因异常一直保持锁。 Monitor.Enter(x) '………… Monitor.Exit(x) SyncLock Me '…………. End SyncLockSyncLock 块通过调用 System.Threading 命名空间中的 Monitor 类的 Enter 方法和 Exit 方法获取和释放独占锁。 SyncLock 语句(在执行一个语句块前获取此块的独占锁。) SyncLock lockobject [ block ] End SyncLocklockobject 必需。 计算结果等于对象引用的表达式。lockobject 的值不能为 Nothing。 必须先创建锁定对象 block 可选。 获取独占锁时要执行的语句块。 End SyncLock 终止 SyncLock 块。 SyncLock 语句可确保多个线程不在同一时间执行语句块。最常见作用是保护数据不被多个线程同时更新。如果操作数据的语句必须在没有中断的情况下完成,请将它们放入 SyncLock 块。 有时将受独占锁保护的语句块称为“临界区”。 Class simpleMessageList Public messagesList() As String = New String(50) {} Public messagesLast As Integer = -1 Private messagesLock As New Object Public Sub addAnotherMessage(ByVal newMessage As String) SyncLock messagesLock messagesLast += 1 If messagesLast < messagesList.Length Then messagesList(messagesLast) = newMessage End If End SyncLock End Sub End Class 例:SyncLock Me造成死锁的情况。 Imports System.Threading Namespace Locking Public Class Locking Private result As Integer = 0 Public Sub CriticalSection() SyncLock Me Console.WriteLine("Entered Thread " & Thread.CurrentThread.GetHashCode.ToString) For i As Integer = 1 To 5 Console.WriteLine("Result = " & result & " ThreadID " & Thread.CurrentThread.GetHashCode.ToString) result += 1 Thread.Sleep(1000) Next i Console.WriteLine("Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString) End SyncLock End Sub Public Overloads Shared Sub Main(ByVal args() As String) Dim e As New Locking() Dim t1 As New Thread(New ThreadStart(AddressOf e.CriticalSection)) t1.Start() Dim t2 As New Thread(New ThreadStart(AddressOf e.CriticalSection)) t2.Start() Console.ReadLine() End Sub End Class End Namespace 说明:MSND建议尽量不要用SyncLock Me。原因是Me的滥用,使得代码变得复杂,甚至互锁、死锁。 例:SyncLock Me与类外锁定对象实例时死锁情况。 Imports System.Threading Namespace TestUseMe Class C1 Private deadlocked As Boolean = True Public Sub LockMe(ByVal o As Object) SyncLock Me While deadlocked '一直为真,故死锁 deadlocked = CType(o,Boolean) Console.WriteLine("Foo: I am locked :(") Thread.Sleep(500) End While End SyncLock End Sub Public Sub DoNotLockMe() Console.WriteLine("I am not locked :)") End Sub End Class Class Program Shared Sub main() Dim c1 As New C1 Dim t1 As New Thread(AddressOf c1.LockMe) '在t1线程中调用LockMe,并将deadlock设为true(将出现死锁) t1.Start() Thread.Sleep(100) SyncLock c1 '在主线程中上锁 c1 c1.DoNotLockMe() '此法未在t1线程上锁,可调用 c1.LockMe(False) '此法已在t1线程上锁(死锁,需改deadlock为假解锁)无法访问 End SyncLock Console.ReadLine() End Sub End Class End Namespace说明:结果(如下),t1线程锁定其中LockMe(ByVal o As Object)方法后死锁,这里主线程锁定c1对象,DoNotLockMe()方法可访问,但LockMe(ByVal o As Object)方法被死锁,所以无法访问,因此只有一个未被锁定的方法输出内容。 另外一个就是t1线程中的SyncLock Me,这个Me应该是c1,但奇怪的是并没有锁全,导致主线程中仍可访问DoNotLockMe(),这是为什么呢? 原因是:尽量用Private(原文:lockobject 表达式应始终计算仅属于您的类的对象。您应该声明一个 Private 对象变量,以保护属于当前实例的数据,或声明一个 Private Shared 对象变量,以保护所有实例共有的数据。)。 例:微软上的例子 Imports System.Threading Module Module1 Class Account Dim thisLock As New Object '随便创建的一个引用型对象实例 Dim balance As Integer '这才是保护的数据 Dim r As New Random() Public Sub New(ByVal initial As Integer) balance = initial End Sub Public Function Withdraw(ByVal amount As Integer) As Integer SyncLock thisLock If balance >= amount Then Console.Write("ThreadID:" & Thread.CurrentThread.ManagedThreadId.ToString) Console.Write(",Balance: " & balance & ",Withdraw:-" & amount) balance = balance - amount Console.WriteLine(" Result: " & balance) Return amount Else Return 0 End If End SyncLock End Function Public Sub DoTransactions() Withdraw(r.Next(1,500)) '取钱 End Sub End Class Sub Main() Dim threads(10) As Thread Dim acc As New Account(1000) '银行总额1000 For i As Integer = 0 To 9 '产生10个线程 Dim t As New Thread(New ThreadStart(AddressOf acc.DoTransactions)) threads(i) = t Next For i As Integer = 0 To 9 '10个线程开始执行 threads(i).Start() Next Console.ReadLine() End Sub End Module (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |