VBNET多线程编程的详细说明Word格式.docx
《VBNET多线程编程的详细说明Word格式.docx》由会员分享,可在线阅读,更多相关《VBNET多线程编程的详细说明Word格式.docx(16页珍藏版)》请在冰豆网上搜索。
![VBNET多线程编程的详细说明Word格式.docx](https://file1.bdocx.com/fileroot1/2022-12/9/8c993a8e-6cd8-4b66-a599-5eb5fe2a0166/8c993a8e-6cd8-4b66-a599-5eb5fe2a01661.gif)
l单独的事务必须等待外部资源,例如远程文件或Internet连接。
例如,某个应用程序跟随Web页面上的链接并下载符合特定条件的文件。
这种应用程序可以同步一个接一个地下载文件或者使用多线程在同一时刻下载多个文件。
多线程的方法比同步方法的效率高得多,因为即使某些线程从远程Web服务器上接收到的响应很慢,文件也可以被下载。
建立新线程
建立线程的最直接的方法是建立线程类的一个新的实例并且使用AddressOf语句替你希望运行的过程传递一个委托。
例如下面的代码运行一个作为单独的线程的叫做SomeTask的子过程。
DimThread1AsNewSomeTask)
'
这儿的代码立即运行
这就是建立和启动线程的全部工作。
调用线程的Start方法后面的任何代码立即执行,不需要等待前面线程的结束。
下表是你能使用的控制单独线程的方法:
上面的大多数方法字面上容易理解,但是安全点(safepoint)的概念对你来说可能是新的。
安全点是代码中的某个位置,在这个位置通用语言运行时可以安全地执行自动无用单元收集(garbagecollection,释放无用变量并恢复内存的过程)。
当调用线程的Abort或Suspend方法时,通用语言运行时分析代码,决定线程停止运行的适当位置。
下表是线程的一些常用的属性:
当建立和管理线程时它的属性和方法很重要。
本文的"
线程同步"
部分将讨论你怎样使用这些属性和方法控制和调整线程。
线程参数和返回值
前面例子中的线程调用没有参数和返回值。
这是使用这种方法建立和运行线程的主要缺点之一。
但是,你可以在类或结构体中包装线程,为运行在单独线程上的过程提供和返回参数。
ClassTasksClass
FriendStrArgAsString
FriendRetValAsBoolean
SubSomeTask()
StrArg字段是一个参数
MsgBox("
TheStrArgcontainsthestring"
&
StrArg)
RetVal=True'
设置返回参数中的返回值
EndSub
EndClass
为了使用这个类,设置存储参数的属性或者字段,接着异步调用需要的方法
SubDoWork()
DimTasksAsNewTasksClass()
DimThread1AsNew
="
SomeArg"
设置作为参数使用的字段
()'
启动新线程
等待线程1结束
显示返回值
Thread1returnedthevalue"
EndSub
手工建立和管理线程最适合于希望很好地控制细节(例如线程的优先级和线程模型)的应用程序。
你可能想象,通过这种方法管理大量的线程是很困难的。
在你需要很多线程时考虑使用线程池来减小复杂程度。
线程池
线程池是多线程的一种形式,在它里面,事务被添加到一个队列,并随着线程的建立自动启动。
有了线程池,你使用希望运行的过程的委托调用方法,VisualBasic.NET就建立线程并运行该过程。
下面的例子演示了怎样使用线程池启动几个事务:
SubDoWork()
DimTPoolAs'
对一个事务排队
(NewSomeLongTask))
对另一个事务排队
(NewAnotherLongTask))
EndSub
当你需要启动很多单独事务而不需要单独设置每个线程的属性时,线程池是很有用的。
每个线程使用默认的栈大小和优先级启动。
默认情况下,每个系统处理器可以运行高达25个线程池线程。
超过限制的线程可以排队,但是直到其它线程结束才能启动。
线程池的一个优点是你能把状态对象中的参数传递给每个事务过程。
如果调用的过程需要一个以上参数,你可以把一个结构体或类的示例转换为Object数据类型。
参数和返回值
从线程池线程返回值有点棘手。
从函数调用返回值的标准方法在这儿是不允许的,因为Sub过程是能被线程池排队的唯一过程类型。
提供参数和返回值的途径是把这些参数,返回值和方法包装进一个包装类。
提供参数和返回值的一个更简单的方法是使用QueueUserWorkItem方法的ByVal状态对象变量。
如果使用该变量传递引用给类的一个实例,实例中的成员能被线程池线程修改并作为返回值使用。
起先可以修改值传递的变量所引用的对象是不明显的,由于只有对象引用被值传递了,它才是可能的。
当你修改对象引用引用的对象的成员时,改变应用到实际类的实例。
结构体不能用于在状态对象内部返回值。
因为结构体是值类型的,异步处理做的改变不会改变原结构体的成员。
当不需要返回值时使用结构体提供参数。
FriendClassStateObj
FriendStrArgAsString
FriendIntArgAsInteger
FriendRetValAsString
EndClass
SubThreadPoolTest()
DimTPoolAsDimStObj1AsNewStateObj()
DimStObj2AsNewStateObj()
设置状态对象中的作为参数的一些字段
=10
Somestring"
=100
Someotherstring"
对一个事务进行排队
(New_
(AddressOfSomeOtherTask),StObj1)
对另一个事务进行排队
(AddressOfAnotherTask),StObj2)
SubSomeOtherTask(ByValStateObjAsObject)
使用状态对象字段作为参数
DimStObjAsStateObj
StObj=CType(StateObj,StateObj)'
转换成正确的类型
StrArgcontainsthestring"
IntArgcontainsthenumber"
CStr)
使用一个字段作为返回值
ReturnValuefromSomeOtherTask"
SubAnotherTask(ByValStateObjAsObject)
使用状态对象作为参数。
状态对象作为Object传递。
把它转换为特定类型使使用更容易
StObj=CType(StateObj,StateObj)
StrArgcontainstheString"
ReturnValuefromAnotherTask"
通用语言运行时自动为排队的线程池事务建立线程,当这些事务完成时释放这些资源。
一旦事务被排队了,这就不是取消事务的容易的方法了。
ThreadPool线程使用多线程单元(MTA)线程模型运行。
如果你希望线程使用单线程单元模型(STA)运行,必须手工建立线程。
线程同步
同步提供了多线程编程的无组织特性和同步处理的有组织次序之间一种折衷的方法。
使用同步技术能够达到的目标:
l在事务必须按特定次序执行的时候,明确地控制代码运行的次序。
l当两个线程在同一时刻共享相同的资源的时候防止错误的发生。
例如,你可以使用同步来引发一个显示过程等待另一个线程上运行的数据检索过程结束。
有两种同步的途径,轮询(polling)和使用同步对象。
轮询是从某个循环中周期性地检查异步调用的状态。
轮询是管理线程的效率最低的方法,因为它周期性检查多样线程属性的状态,浪费了资源。
例如当轮询查看某个线程是否终止时会使用IsAlive属性。
使用这个属性必须注意,因为有效的线程不是一定运行的。
你可以使用ThreadState属性获得线程状态的更多详细信息。
因为在给定的时刻线程可能有一个以上的状态,ThreadState中存储的值可能是枚举中值的组合。
因此轮询时你必须仔细检查所有的相关线程状态。
例如,如果线程的状态显示它不是Running的,它有可能结束了。
另一方面,它也可能挂起或休眠了。
你可以想象,轮询为了换取对线程次序的控制牺牲了多线程的一些优点。
效率更高的途径是使用Join方法控制线程。
Join引发调用过程等待一个线程完成或者超时(如果指定了超时值)。
Join这个名字基于建立新线程,它是执行路径中的分叉。
你使用Join方法把单独的执行路径合并成单个线程。
图1.线程
有一点必须清楚,Join是同步的或阻塞的调用。
一旦你调用Join或等待句柄的等待方法,调用过程会停止并等待线程发出完成信号。
SubJoinThreads()
DimThread1AsNewSomeTask)
()
等待该线程结束
Threadisdone"
)
这些简单的控制线程的方法对管理少量的线程是有用的,但是在大型项目中使用困难。
下一部分讨论用于同步的一些高级技术。
高级同步技术
多线程应用程序通常使用等待处理和监视对象来同步多个线程。
下表是.NET框架组件中能用于同步线程的一些类:
等待句柄
等待句柄是把某个线程的状态信号发送给另一个线程的对象。
当线程需要独占访问某种资源时,它们可以使用等待句柄通知其它线程。
其它线程必须等待这些资源,直到等待句柄不再使用。
等待句柄有两种状态:
signaled和nonsignaled。
不属于任何线程的等待句柄状态为signaled。
属于某个线程的等待句柄的状态是nonsignaled。
线程通过调用一个等待方法(例如WaitOne、WaitAny或WaitAll)来请求等待句柄的所有权。
等待方法也是阻塞调用,与独立线程的Join方法类似。
l如果其它线程没有拥有等待句柄,该调用立即返回True,等待线程的状态变为nonsignaled,拥有等待句柄的线程继续运行。
l如果某个线程调用等待句柄的一个等待方法,但是等待句柄属于另一个线程,发出调用的线程要么等待一个特定时间(如果指定了超时值)或者等待不确定的时长(没有指定超时值)直到其它线程释放等待句柄。
如果设置了超时值并且等待句柄在期满前被释放了,该调用将返回True。
否则,该调用返回False,发送调用的线程继续运行。
当拥有等待句柄的线程完成后或者它们再也不需要等待句柄时,它们调用Set方法。
其它线程可以通过调用Reset方法或WaitOne、WaitAll、WaitAny把等待句柄的状态复位成nonsignaled,并且成功地等待某个线程调用Set。
当某个等待线程被释放后系统自动把AutoResetEvent句柄复位成nonsignaled。
如果没有线程在等待,该事件对象的状态仍然为signaled。
VisualBasic.NET中通常使用三类等待句柄:
互斥对象、ManualResetEvent和AutoResetEvent。
后两种通常用于同步事件。
互斥对象
互斥对象都是同步对象,它们只能在一个时刻由一个线程拥有。
实际上,互斥这个名字衍生自互斥对象的所有权是相互排斥的。
当线程请求独占访问某种资源时,它们请求互斥对象的所有权。
因为在某个时刻只有一个线程能拥有一个互斥对象,其它线程在使用资源前必须等待互斥对象的所有权。
WaitOne方法引发一个调用线程等待互斥对象的所有权。
如果拥有互斥对象的线程正常终止,该互斥对象的状态就被设置为signaled,下一个线程获得它的所有权。
同步事件
同步事件用于通知其它的线程发生了某种事情或者某种资源可用。
不要被它使用了"
事件"
这个词迷惑了。
同步事件与其它的VisualBasic事件不同,它是真正的等待句柄。
与其它的等待句柄类似,同步事件有两种状态signaled和nonsignaled。
调用同步事件的某个等待方法的线程必须等待,直到其它线程调用Set方法给事件发信号。
有两个同步事件类。
线程使用Set方法把ManualResetEvent实例的状态设置为signaled。
线程使用Reset方法或控制返回等待WaitOne调用把实例的状态设置为nonsignaled。
AutoResetEvent类的实例也可以使用Set设置为signaled,但是只要通知等待线程事件变为signaled,它们自动返回到nonsignaled。
下面的例子使用AutoResetEvent类同步线程池事务。
SubStartTest()
DimATAsNewAsyncTest()
ClassAsyncTest
PrivateSharedAsyncOpDoneAsNewSubStartTask()
DimTpoolAsDimargAsString="
SomeArg"
AddressOfTask),arg)'
等待该线程调用Set
Threadisdone."
SubTask(ByValArgAsObject)
Threadisstarting."
等待4秒.
Thestateobjectcontainsthestring"
CStr(Arg))
发信号表明该线程完成了
监视对象和同步锁
监视对象确保代码块的运行不被运行在其它线程中的代码打断。
换句话说,其它线程中的代码不能运行,直到被同步的代码块结束。
在VisualBasic.NET中使用SyncLock关键字来简化监视对象的访问。
在VisualC#.NET中使用Lock关键字。
例如,假定你有一个程序,它重复地、异步读取数据并显示结果。
使用优先多任务操作系统,正在运行的线程可以因为操作系统允许其它的线程运行而被打断。
如果没有同步,数据正在显示时,显示数据的对象被其它的线程修改,有可能得到的是部分更新的数据视图。
SyncLock保证一段代码持续运行,不被打断。
下面的例子显示了怎样使用SyncLock给显示过程提供数据对象的独占访问。
ClassDataObject
PublicObjTextAsString
PublicObjTimeStampAsDate
SubRunTasks()
DimMyDataObjectAsNewDataObject()
ReadDataAsync(MyDataObject)
SyncLockMyDataObject
DisplayResults(MyDataObject)
EndSyncLock
SubReadDataAsync(ByRefMyDataObjectAsDataObject)
添加异步读取和处理数据的代码
SubDisplayResults(ByValMyDataObjectAsDataObject)
添加显示结果的代码
当有一段代码不能被某个独立的线程中运行的代码打断时使用SyncLock。
Interlocked类
你可以使用Interlocked类的方法防止多个线程同时更新或比较同一个值的问题发生。
这个类的方法让你安全地增加、减少、交换和比较来自任何线程的值。
下面的例子演示了怎样使用Increment方法增加一个运行在独立线程上的多个过程共享的变量的值。
SubThreadA(ByRefIntAAsInteger)
Sub
SubThreadB(ByRefIntAAsInteger)
Sub
ReaderWriter锁
在有些情况下,你可能希望只在写数据时锁定资源,在数据没有更新完前允许多个客户同时读数据。
某个线程正在修改资源时,ReaderWriterLock类加强了对该资源的独占访问,但是允许读取资源的非独占访问。
ReaderWriter锁是排他锁的一个有用的备选方案,排他锁引起其它线程等待,即使这些线程不需要更新数据。
下面的例子演示了怎样使用ReaderWriter调整来自多个线程的读和写操作。
ClassReadWrite
ReadData和WriteData方法可以被多个线程安全地调用
PublicReadWriteLockAsNewReadData()
'
这个过程从数据源读取信息。
在允许其它线程调用ReadData时,读取锁放置任何数据写入直到读取完成
Try
'
此处执行数据操作
Finally
()'
释放读取锁
EndTry
SubWriteData()
这个过程向数据源写信息。
写入锁防止数据被读取或者写入知道线程完成写操作。
Try
此处执行写操作
Finally
释放写入锁
EndTry
死锁
在多线程应用程序中线程同步是无价之宝,但是始终有多个线程彼此等待的死锁的危险。
类似汽车停在四条路上,彼此等待对方前进,死锁使所有动作停止。
不用说,避免死锁很重要。
有很多种途径会造成死锁,同样有多种方法可以避免它们。
尽管本文没有足够的篇幅讨论死锁相关的问题,但是重要的一点是细心计划是避免死锁的关键。
你可以在开始编码前用图解法表示应用程序,预计死锁的情形。
线程计时器
类对于在独立的线程上周期性地运行事务是很有用的。
例如,你可以使用线程计时器检查数据库的状态和完整性或者备份关键文件。
下面的例子每两秒启动一个事务,并使用一个标记来初始化停止计时器的Dispose方法。
这个例子把状态发送到输出窗口,因此在测试代码前你可以通过按Control+Alt+O使窗口可见。
ClassStateObjClass
为TimerTask调用保持参数
PublicSomeValueAsInteger
PublicTimerReferenceAsPublicTimerCanceledAsBoolean
SubRunTimer()
DimStateObjAsNewStateObjClass()
=False
=1
DimTimerDelegateAsNew(AddressOfTimerTask)
建立一个定时器每2秒调用一个过程。
注意:
这儿没有Start方法,计时器在实例被建立时启动它
DimTimerItemAsNewStateObj,2000,2000)
=TimerItem'
为Dispose保存一个引用
While<
10'
执行10次
等待1秒
EndWhile
=True'
请求计时器对象的Dispose
SubTimerTask(ByValStateObjAsObject)
DimStateAsStateObjClass=CType(StateObj,StateObjClass)
DimxAsInteger
使用interlocked类增加计数器变量的值
("
Launchednewthread"
Now)
IfThen'
请求Dispose
Done"
EndIf
取消事务
多线程的优点之一是应用程序的用户界面保持响应时,事务可以在其它的线程上运行。
同步事件和作为标记的字段通常用于通知其它线程你希望停止它。
下面的例子使用同步事件取消一个事务。
为了使用这个例子,给项目添加下面的模块。
调用()启动一个线程,调用()取消一个或多个正在运行的线程。
ModuleStartCancel
PublicCancelThreadAsNewPublicThreadisCanceledAsNewPrivateSubSomeLongTask