VB NET多线程编程的详细说明完整版.docx
- 文档编号:4476000
- 上传时间:2022-12-01
- 格式:DOCX
- 页数:16
- 大小:54.12KB
VB NET多线程编程的详细说明完整版.docx
《VB NET多线程编程的详细说明完整版.docx》由会员分享,可在线阅读,更多相关《VB NET多线程编程的详细说明完整版.docx(16页珍藏版)》请在冰豆网上搜索。
VBNET多线程编程的详细说明完整版
VB .NET多线程编程的详细说明
作者:
陶刚 整理:
更新时间:
2011-4-1
介绍
传统的VisualBasic开发人员已经建立了同步应用程序,在这些程序中事务按顺序执行。
尽管由于多个事务多多少少地同时运行使多线程应用程序效率更高,但是使用先前版本的VisualBasic很难建立这类程序。
多线程程序是可行的,因为操作系统是多任务的,它有模拟同一时刻运行多个应用程序的能力。
尽管多数个人计算机只有一个处理器,但是现在的操作系统还是通过在多个执行代码片断之间划分处理器时间提供了多任务。
线程可能是整个应用程序,但通常是应用程序可以单独运行的一个部分。
操作系统根据线程的优先级和离最近运行的时间长短给每一个线程分配处理时间。
多线程对于时间密集型事务(例如文件输入输出)应用程序的性能有很大的提高。
但是也有必须细心的地方。
尽管多线程能提高性能,但是每个线程还是需要用附加的内存来建立和处理器时间来运行,建立太多的线程可能降低应用程序的性能。
当设计多线程应用程序时,应该比较性能与开销。
多任务成为操作系统的一部分已经很久了。
但是直到最近VisualBasic程序员才能使用无文档记录特性(undocumented)或者间接使用COM组件或者操作系统的异步部分执行多线程事务。
.NET框架组件为开发多线程应用程序,在System.Threading名字空间中提供了全面的支持。
本文讨论多线程的好处以及怎样使用VisualBasic.NET开发多线程应用程序。
尽管VisualBasic.NET和.NET框架组件使开发多线程应用程序更容易,但是本文作了调整使其适合高级读者和希望从早期VisualBasic转移到VisualBasic.NET的开发人员。
多线程处理的优点
尽管同步应用程序易于开发,但是它们的性能通常比多线程应用程序低,因为一个新的事务必须等待前面的事务完成后才能开始。
如果完成某个同步事务的时间比预想的要长,应用程序可能没有响应。
多线程处理可以同时运行多个过程。
例如,字处理程序能够在继续操作文档的同时执行拼写检查事务。
因为多线程应用程序把程序分解为独立的事务,它们能通过下面的途径充分提高性能:
l多线程技术可以使程序更容易响应,因为在其它工作继续时用户界面可以保持激活。
l当前不忙的事务可以把处理器时间让给其它事务。
l花费大量处理时间的事务可以周期性的把时间让给其它的事务。
l事务可以在任何时候停止。
l可以通过把单独事务的优先级调高或调低来优化性能。
明确地建立多线程应用程序的决定依赖于几个因素。
多线程最适合下面的情况:
l时间密集或处理密集的事务妨碍用户界面。
l单独的事务必须等待外部资源,例如远程文件或Internet连接。
例如,某个应用程序跟随Web页面上的链接并下载符合特定条件的文件。
这种应用程序可以同步一个接一个地下载文件或者使用多线程在同一时刻下载多个文件。
多线程的方法比同步方法的效率高得多,因为即使某些线程从远程Web服务器上接收到的响应很慢,文件也可以被下载。
建立新线程
建立线程的最直接的方法是建立线程类的一个新的实例并且使用AddressOf语句替你希望运行的过程传递一个委托。
例如下面的代码运行一个作为单独的线程的叫做SomeTask的子过程。
DimThread1AsNewSystem.Threading.Thread(AddressOfSomeTask)
Thread1.Start
'这儿的代码立即运行
这就是建立和启动线程的全部工作。
调用线程的Start方法后面的任何代码立即执行,不需要等待前面线程的结束。
下表是你能使用的控制单独线程的方法:
上面的大多数方法字面上容易理解,但是安全点(safepoint)的概念对你来说可能是新的。
安全点是代码中的某个位置,在这个位置通用语言运行时可以安全地执行自动无用单元收集(garbagecollection,释放无用变量并恢复内存的过程)。
当调用线程的Abort或Suspend方法时,通用语言运行时分析代码,决定线程停止运行的适当位置。
下表是线程的一些常用的属性:
当建立和管理线程时它的属性和方法很重要。
本文的"线程同步"部分将讨论你怎样使用这些属性和方法控制和调整线程。
线程参数和返回值
前面例子中的线程调用没有参数和返回值。
这是使用这种方法建立和运行线程的主要缺点之一。
但是,你可以在类或结构体中包装线程,为运行在单独线程上的过程提供和返回参数。
ClassTasksClass
FriendStrArgAsString
FriendRetValAsBoolean
SubSomeTask()
'StrArg字段是一个参数
MsgBox("TheStrArgcontainsthestring"&StrArg)
RetVal=True'设置返回参数中的返回值
EndSub
EndClass
'为了使用这个类,设置存储参数的属性或者字段,接着异步调用需要的方法
SubDoWork()
DimTasksAsNewTasksClass()
DimThread1AsNewSystem.Threading.Thread(AddressOfTasks.SomeTask)
Tasks.StrArg="SomeArg"'设置作为参数使用的字段
Thread1.Start()'启动新线程
Thread1.Join()'等待线程1结束
'显示返回值
MsgBox("Thread1returnedthevalue"&Tasks.RetVal)
EndSub
手工建立和管理线程最适合于希望很好地控制细节(例如线程的优先级和线程模型)的应用程序。
你可能想象,通过这种方法管理大量的线程是很困难的。
在你需要很多线程时考虑使用线程池来减小复杂程度。
线程池
线程池是多线程的一种形式,在它里面,事务被添加到一个队列,并随着线程的建立自动启动。
有了线程池,你使用希望运行的过程的委托调用Threadpool.QueueUserWorkItem方法,VisualBasic.NET就建立线程并运行该过程。
下面的例子演示了怎样使用线程池启动几个事务:
SubDoWork()
DimTPoolAsSystem.Threading.ThreadPool
'对一个事务排队
TPool.QueueUserWorkItem(NewSystem.Threading.WaitCallback(AddressOfSomeLongTask))
'对另一个事务排队
TPool.QueueUserWorkItem(NewSystem.Threading.WaitCallback(AddressOfAnotherLongTask))
EndSub
当你需要启动很多单独事务而不需要单独设置每个线程的属性时,线程池是很有用的。
每个线程使用默认的栈大小和优先级启动。
默认情况下,每个系统处理器可以运行高达25个线程池线程。
超过限制的线程可以排队,但是直到其它线程结束才能启动。
线程池的一个优点是你能把状态对象中的参数传递给每个事务过程。
如果调用的过程需要一个以上参数,你可以把一个结构体或类的示例转换为Object数据类型。
参数和返回值
从线程池线程返回值有点棘手。
从函数调用返回值的标准方法在这儿是不允许的,因为Sub过程是能被线程池排队的唯一过程类型。
提供参数和返回值的途径是把这些参数,返回值和方法包装进一个包装类。
提供参数和返回值的一个更简单的方法是使用QueueUserWorkItem方法的ByVal状态对象变量。
如果使用该变量传递引用给类的一个实例,实例中的成员能被线程池线程修改并作为返回值使用。
起先可以修改值传递的变量所引用的对象是不明显的,由于只有对象引用被值传递了,它才是可能的。
当你修改对象引用引用的对象的成员时,改变应用到实际类的实例。
结构体不能用于在状态对象内部返回值。
因为结构体是值类型的,异步处理做的改变不会改变原结构体的成员。
当不需要返回值时使用结构体提供参数。
FriendClassStateObj
FriendStrArgAsString
FriendIntArgAsInteger
FriendRetValAsString
EndClass
SubThreadPoolTest()
DimTPoolAsSystem.Threading.ThreadPool
DimStObj1AsNewStateObj()
DimStObj2AsNewStateObj()
'设置状态对象中的作为参数的一些字段
StObj1.IntArg=10
StObj1.StrArg="Somestring"
StObj2.IntArg=100
StObj2.StrArg="Someotherstring"
'对一个事务进行排队
TPool.QueueUserWorkItem(NewSystem.Threading.WaitCallback_
(AddressOfSomeOtherTask),StObj1)
'对另一个事务进行排队
TPool.QueueUserWorkItem(NewSystem.Threading.WaitCallback_
(AddressOfAnotherTask),StObj2)
EndSub
SubSomeOtherTask(ByValStateObjAsObject)
'使用状态对象字段作为参数
DimStObjAsStateObj
StObj=CType(StateObj,StateObj)'转换成正确的类型
MsgBox("StrArgcontainsthestring"&StObj.StrArg)
MsgBox("IntArgcontainsthenumber"&CStr(StObj.IntArg))
'使用一个字段作为返回值
StObj.RetVal="ReturnValuefromSomeOtherTask"
EndSub
SubAnotherTask(ByValStateObjAsObject)
'使用状态对象作为参数。
状态对象作为Object传递。
把它转换为特定类型使使用更容易
DimStObjAsStateObj
StObj=CType(StateObj,StateObj)
MsgBox("StrArgcontainstheString"&StObj.StrArg)
MsgBox("IntArgcontainsthenumber"&CStr(StObj.IntArg))
'使用一个字段作为返回值
StObj.RetVal="ReturnValuefromAnotherTask"
EndSub
通用语言运行时自动为排队的线程池事务建立线程,当这些事务完成时释放这些资源。
一旦事务被排队了,这就不是取消事务的容易的方法了。
ThreadPool线程使用多线程单元(MTA)线程模型运行。
如果你希望线程使用单线程单元模型(STA)运行,必须手工建立线程。
线程同步
同步提供了多线程编程的无组织特性和同步处理的有组织次序之间一种折衷的方法。
使用同步技术能够达到的目标:
l在事务必须按特定次序执行的时候,明确地控制代码运行的次序。
l当两个线程在同一时刻共享相同的资源的时候防止错误的发生。
例如,你可以使用同步来引发一个显示过程等待另一个线程上运行的数据检索过程结束。
有两种同步的途径,轮询(polling)和使用同步对象。
轮询是从某个循环中周期性地检查异步调用的状态。
轮询是管理线程的效率最低的方法,因为它周期性检查多样线程属性的状态,浪费了资源。
例如当轮询查看某个线程是否终止时会使用IsAlive属性。
使用这个属性必须注意,因为有效的线程不是一定运行的。
你可以使用ThreadState属性获得线程状态的更多详细信息。
因为在给定的时刻线程可能有一个以上的状态,ThreadState中存储的值可能是System.Threading.Threadstate枚举中值的组合。
因此轮询时你必须仔细检查所有的相关线程状态。
例如,如果线程的状态显示它不是Running的,它有可能结束了。
另一方面,它也可能挂起或休眠了。
你可以想象,轮询为了换取对线程次序的控制牺牲了多线程的一些优点。
效率更高的途径是使用Join方法控制线程。
Join引发调用过程等待一个线程完成或者超时(如果指定了超时值)。
Join这个名字基于建立新线程,它是执行路径中的分叉。
你使用Join方法把单独的执行路径合并成单个线程。
图1.线程
有一点必须清楚,Join是同步的或阻塞的调用。
一旦你调用Join或等待句柄的等待方法,调用过程会停止并等待线程发出完成信号。
SubJoinThreads()
DimThread1AsNewSystem.Threading.Thread(AddressOfSomeTask)
Thread1.Start()
Thread1.Join()'等待该线程结束
MsgBox("Threadisdone")
EndSub
这些简单的控制线程的方法对管理少量的线程是有用的,但是在大型项目中使用困难。
下一部分讨论用于同步的一些高级技术。
高级同步技术
多线程应用程序通常使用等待处理和监视对象来同步多个线程。
下表是.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()
AT.StartTask()
EndSub
ClassAsyncTest
PrivateSharedAsyncOpDoneAsNewSystem.Threading.AutoResetEvent(False)
SubStartTask()
DimTpoolAsSystem.Threading.ThreadPool
DimargAsString="SomeArg"
Tpool.QueueUserWorkItem(NewSystem.Threading.WaitCallback(_
AddressOfTask),arg)'对一个事务进行排队
AsyncOpDone.WaitOne()'等待该线程调用Set
MsgBox("Threadisdone.")
EndSub
SubTask(ByValArgAsObject)
MsgBox("Threadisstarting.")
System.Threading.Thread.Sleep(4000)'等待4秒.
MsgBox("Thestateobjectcontainsthestring"&CStr(Arg))
AsyncOpDone.Set()'发信号表明该线程完成了
EndSub
EndClass
监视对象和同步锁
监视对象确保代码块的运行不被运行在其它线程中的代码打断。
换句话说,其它线程中的代码不能运行,直到被同步的代码块结束。
在VisualBasic.NET中使用SyncLock关键字来简化监视对象的访问。
在VisualC#.NET中使用Lock关键字。
例如,假定你有一个程序,它重复地、异步读取数据并显示结果。
使用优先多任务操作系统,正在运行的线程可以因为操作系统允许其它的线程运行而被打断。
如果没有同步,数据正在显示时,显示数据的对象被其它的线程修改,有可能得到的是部分更新的数据视图。
SyncLock保证一段代码持续运行,不被打断。
下面的例子显示了怎样使用SyncLock给显示过程提供数据对象的独占访问。
ClassDataObject
PublicObjTextAsString
PublicObjTimeStampAsDate
EndClass
SubRunTasks()
DimMyDataObjectAsNewDataObject()
ReadDataAsync(MyDataObject)
SyncLockMyDataObject
DisplayResults(MyDataObject)
EndSyncLock
EndSub
SubReadDataAsync(ByRefMyDataObjectAsDataObject)
'添加异步读取和处理数据的代码
EndSub
SubDisplayResults(ByValMyDataObjectAsDataObject)
'添加显示结果的代码
EndSub
当有一段代码不能被某个独立的线程中运行的代码打断时使用SyncLock。
Interlocked类
你可以使用Interlocked类的方法防止多个线程同时更新或比较同一个值的问题发生。
这个类的方法让你安全地增加、减少、交换和比较来自任何线程的值。
下面的例子演示了怎样使用Increment方法增加一个运行在独立线程上的多个过程共享的变量的值。
SubThreadA(ByRefIntAAsInteger)
System.Threading.Interlocked.Increment(IntA)
EndSub
SubThreadB(ByRefIntAAsInteger)
System.Threading.Interlocked.Increment(IntA)
EndSub
ReaderWriter锁
在有些情况下,你可能希望只在写数据时锁定资源,在数据没有更新完前允许多个客户同时读数据。
某个线程正在修改资源时,ReaderWriterLock类加强了对该资源的独占访问,但是允许读取资源的非独占访问。
ReaderWriter锁是排他锁的一个有用的备选方案,排他锁引起其它线程等待,即使这些线程不需要更新数据。
下面的例子演示了怎样使用ReaderWriter调整来自多个线程的读和写操作。
ClassReadWrite
'ReadData和WriteData方法可以被多个线程安全地调用
PublicReadWriteLockAsNewSystem.Threading.ReaderWriterLock()
SubReadData()
'这个过程从数据源读取信息。
在允许其它线程调用ReadData时,读取锁放置任何数据写入直到读取完成
ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
Try
'此处执行数据操作
Finally
ReadWriteLock.ReleaseReaderLock()'释放读取锁
EndTry
EndSub
SubWriteData()
'这个过程向数据源写信息。
写入锁防止数据被读取或者写入知道线程完成写操作。
ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite)
Try
'此处执行写操作
Finally
ReadWriteLock.ReleaseWriterLock()'释放写入锁
EndTry
EndSub
EndClass
死
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- VB NET多线程编程的详细说明完整版 NET 多线程 编程 详细 说明 完整版
![提示](https://static.bdocx.com/images/bang_tan.gif)