VB.net多线程入门

出处 http://songnbell.blog.sohu.com/61817521.html
最近数据库编程中遇到多线程问题,找了些入门资料,没有中文的,找到一篇英文的.net多线程入门文章,于是翻译下来,有些删节和改动,文章是01年写的,由于.net版本的变化导致其中有些类,方法等发生了变化,但是多线程的思想是不变的。
恩,开始:
VB.net可以在你的程序中创建线程的能力是一个值得注意的新特征。(Visual C++的开发者们已经在他们的代码中使用了多年的多线程,而VB6要实现同样的效果却困难重重。)
下面的代码使用的是VB.net,当然了,使用C#也能得到同样的结果。
什么是线程?
我们首先要回答的问题是:“什么是线程?”,好吧,简单点说,线程就像是一个进程中运行了两个程序。所以你编写的软件最少也会包含一个线程——主应用线程。
首先要说一下,你电脑上运行的一个程序的有效实例是一个进程。比如你正开着Word和Excel程序,它们分别在各自独立的进程中运行。
多线程应用的一个典型例子是Word程序里的拼写检查。一个线程(主应用线程)允许你往文件里打字,另一个线程一直运行着,盯着你都打了些什么字并检查其中出现的错误。
使用线程的理由很简单——它可以改善你的程序执行时的表现,进一步说是使你的程序让用户用起来更舒服。现代的计算机系统被设计为可在同一时间干很多活,再拿Word程序举例,一直检查你正在打什么字对它来说很简单。事实上,和我们的打字速度相比,Word程序工作起来不知快多少倍,它还有大量的剩余处理能力。引入线程能够使Word充分利用它的剩余处理能力,使你的使用体验更棒。
另一个例子是Internet Explorer。当IE从网上得到一个资源(比如一张网页或一幅图片),下载资源这项工作是在一个单独的线程中执行的,这么做的结果是你不用等到IE将网页完全下载完后就可以提前看到网页显示。例如,它可以下载组成网页的HTML代码的时候让主应用线程显示目前下载到的网页内容,同时启动多条线程去下载这个网页的图片。尽管它正忙着下载剩余数据,你仍然可以上下滚动网页浏览。
所以作为一个.net开发者,你要使用线程吗?如果你要开发一个桌面应用程序,毫无疑问,利用线程你能轻易找到多种方法改善你的程序UI。如果你开发服务器应用程序,尽管线程仍然有许多应用领域,但不是对每一项工作都适用。
一个典型的服务器应用程序的例子是对从不同的数据源收集到的信息进行比较,想象一下你要在一个类里创建一个方法,这个类从散布在internet上的5、6个web services中获取响应,其中每一个web services都有一个响应时间(这个响应时间取决于3方面:1.这个服务的繁忙程度。2.连接链路的质量好坏。3.这个服务为获取数据所必需做的事情。),在从所有的服务器那里获取响应之前你无法从这个方法中得到返回值。如果没有线程,你要顺序完成这些工作,例如,你向第一台服务器提出请求,等待它的响应,然后是第二台,继续等待,如此反复。如果有了线程,你可以同时执行6个服务器请求操作,然后得到所有的服务器的答复后进行比较。请求者只需要等待那个最长的服务器响应时间就可以做出比较而不是以前那样需要等待6个响应时间之和。
同步
如果你是一个从没有写过多线程代码的读者,你可能会想:“这看起来一点都不难啊。”事实上,编写稳定的多线程代码很难。
如你所知,Windows是一个多任务操作系统,意思是说同一时刻可以做很多工作,传统上可以被认为在同一时间能够运行很多进程,事实也是如此。如果你在用Windows2000系统的电脑看这篇文章,它的前台或后台同时运行了许多服务,每一个单独的程序都可以看作一个进程。
绝大多数多任务操作系统都是孤立地执行进程,这种策略可以使同一台计算机上运行的进程之间不会相互干扰(这种干扰有可能是意外的,也可能是故意的),进程的独立运行是通过禁止一个进程访问分配给另一个进程的内存空间来实现的。
这意味着,如果你过去习惯编写单线程的应用程序,那么这个程序只有一部分可以在任意时刻访问你正在使用的内存。假设你有一个类,并且在里面定义了一个成员变量,如果你想读取或改变这个变量的值,
多线程程序就不是上述情况了,我们唐突地引入一个线程同步的概念,你要做的就是和另一个“进程”共享内存,如果你要改变变量的值,你只要确保没有其它进程正在使用它。
锁和等待状态
这里有几行代码:
Dim a As Integer
a = m_MyVariable
a += 1
m_MyVariable = a
假设你有两个线程试图使用这段代码,m_MyVariable在初始化对象时被赋值为1,我们要做的不是使用一个线程给m_MyVariable的值增加2,让它的结果变成3,我们想使用2个线程,每个线程给m_MyVariable的值增加1,最终结果仍然得到3 ,但是我们遇到一个问题:在这两个线程全部执行完以后,m_MyVariable的结果不是3。
在这种情况中,我们的两个线程都试图访问和改变m_MyVariable,如果两个线程同一时间执行这段代码,那么每一个线程都会得到m_MyVariable的值为1并将这个值储存到a,然后两个线程都对a增加1,再把结果重新放回m_MyVariable里面,当这两个线程都完成了它们的工作,m_MyVariable的值是2,我们的算法不能正常工作。
我们需要做的是对不希望同步访问的代码进行阻塞来确保同一时间两个线程中只有一个线程可以对代码进行访问。我们在代码外面加一把锁,只允许一个线程打开锁进入代码,其它想打开锁进去的线程只能等待直到前面的线程把锁释放掉(我们说它们被“阻塞”了),然后等待进去的线程中的一个线程可以打开它,继续…等等。
这种锁的类型被称为“临界区”,在不需要锁的时候上锁会因为制造了瓶颈而对程序运行效率产生影响,假设有这样的代码:
Dim a As Integer
a = m_MyVariable
MsgBox (“The value is” & a.ToString)
如果你把这段代码放入临界区,任意时刻只有一个线程可以访问它。那么,以我们的观点来看,任意时间的2个线程同时访问它的危害在哪里呢?哪个线程都没有改变什么——它们仅仅是读取了一个变量值而已。在这种情况下使用临界区毫无理由地制造了一个瓶颈,把使用线程的好处全部抹杀掉了!
我们需要的是一个叫做“读者/作者锁”的东西,这种类型的锁只需要从你那里得到一些信息,却可以降低瓶颈带来的影响。假设有两个线程都想同一时间对m_MyVariable进行读取,我们要做的是让每一个线程都打开锁执行操作,并把锁状态置为“正在读取”。
假设还有一个线程想修改m_MyVariable的值,这种情况下我们需要阻塞代码达到独占访问的目的,我们也不想让任何线程读取准备修改的值,也不想让其它线程也来修改这个值,这种情况下,我们让修改变量的线程打开锁执行操作,并把锁的状态置为“正在写入”。
上面两种情况下,如果一个线程想“读”的时候正有线程“写”,那么等锁被释放后再读;同样地,如果想“写”的时候正有线程“读”或者“写”,就先把线程阻塞,等所有的锁被释放后再写。
“读者/作者锁”提供一种更有意义的锁机制,它只在需要的时候产生瓶颈。
尽管我们列举的例子没什么真正意义,但它说明你在使用线程的时候必须仔细考虑同步的问题。在Visual C++里,编写没有带锁和阻塞的代码经常会导致可怕的崩溃,使用VC++的好处是在这样的底层编代码所导致的崩溃能够容易地指出你的错误所在。在VB .net里,你在一个比底层高些的层次上工作,崩溃的机会小许多,也可以为你处理大部分问题,但如果出现崩溃,你要花更多精力找出崩溃的原因,在VB .net里找出不恰当的同步带来的问题显得更加困难,因此开始学习的时候就应该更充分地考虑同步问题。
一个例子
我们接下来看一个例子:如何使用线程创建一个简单的桌面应用程序来统计文章一部分的字数与词数。
所有.net里的线程方面的类都放在名为System. Threading的名字空间内,我们要创建的线程类是System. Threading. Thread类。
我们来描述一下完成后的程序:当用户点击“启动线程”按钮,我们就创建了16个线程(为什么是16没有理由——我们仅仅是想用一些线程创建有趣的实例,从2个到200个都无所谓,虽然你会看见打开了好多个窗口,但这对我们的程序并没有任何改进,到底使用多少个线程没有硬性规定,按照具体问题自己决定吧),其中8个线程属于类WordCounter,它们负责统计文章中的单词数,其余线程属于类CharCounter,它们负责统计文章的字母数。当用户点击“停止线程”或关闭程序,我们想平稳地关闭所有线程。
上述两个类有许多共性(它们都要访问文章中的内容,都要报告给用户结果,都要被主应用线程控制),所以我们创建一个名为MyWorkThread的类,WordCounter和CharCounter都从它派生。
另一个重要问题是我们只想让每个线程被创建一次。创建线程需要额外的系统开销所以我们尽量少用它(.net支持线程池,但超出了本文的讨论范围)。
当一个线程被阻塞后,它将进入一个完全的等待状态,它将彻底放弃处理器的使用。所以我们可以按照我们的意愿拥有许多线程而不会影响计算机的性能表现(这么说有点理想化,但大体上正确)。
因为用户修改文章的速度不可能太快(即使这个用户打字速度真的很快,我们也有信心保证计算机的处理速度比他更快),所以我们不希望我们的线程一直在运行着,而是让它们大部分时间处于休眠状态,只在有需要时醒来处理一下工作就行了。我们将在基类MyWorkThread里创建这个功能。
创建工程
要创建工程,打开VS.NET后新建一个Visual Basic的Windows应用程序,我们的工程名字就叫DesktopDemo。
创建窗体
我们准备创建一个由许多Text Box控件组成的表格来显示每一个线程所处的状态,窗体上没有放置多余的控件,只有显示线程状态的Text Box控件(起名叫做txtText)和控制线程开启和停止的按钮(分别叫做cmdStartThreads和cmdStopThreads)。
我们第一步工作就是给这个窗体加一个成员变量(用来保存由Text Box控件组成的数组)。
Public Class Form1
Inherits Systems.Winforms.Form
‘创建这些可以保证我们能够跟随线程的踪迹
Const NUMTHREADS As Integer = 16
Dim m_ThreadOutputs(NUMTHREADS) As TextBox
如上所示,我们用一个常量NUMTHREADS表示线程数目,在窗体被创建的时候同时创建这些文本框,改变NUMTHREADS的值就会自动地改变程序的UI。
为了创建主窗体,VB调用了一个叫做InitializeComponent的函数…(原文用了大量篇幅介绍主窗体中各控件在主窗体大小变化时的相对位置和大小相应变化的代码编写方法,这里为了突出主题省略这些部分)。
创建MyWorkThread
MyWorkThread类需要下列控件:
1、 m_Thread——对象System.Threading.Thread的一个实例,使我们能够控制线程。
2、 m_Pause——对象System.Threading.ManualResetEvent的一个实例,可以使我们“暂停”和“继续运行”线程。
3、 m_IsCancelled——一个布尔标志提示我们一个线程是否已经被删除。
4、 m_Control——在主窗体上动态创建Text Box控件用到的一个参数。
创建一个叫做MyWorkThread的类,并加上下面的名字空间:
Imports System
Imports System.Threading
Imports System.WinForms
然后在类定义中加上成员变量,同时加上一个关键字MustInherit,这意味着我们不可以对它进行实例化,它的作用就是让别的类继承它(事实上,我们给类MyWorkThread加入关键字MustInherit是因为我们根本不需要创建MyWorkThread对象的实例)。
Public MustInherit Class MyWorkThread
Private m_Thread As Thread
Private m_Pause As New ManualResetEvent(True)
Private m_IsCancelled As Boolean
Private m_Control As TextBox
为了读取和设置Text Box控件信息,我们添加了一个属性(叫做Control)。
Public Property Control() As TextBox
Get
Return m_control
End Get
Set
m_control = value
End Set
End Property
我们创建了另一个方法,用来改变文本框内的内容:
Public Property ControlText() As String
Get
Return m_Control.text
End Get
Set
m_control.Text = Me.ToString & ": " & value
End Set
End Property
最后我们需要添加一个方法,它给主程序窗体返回一个参数。Text Box的父成员能给我们返回一个WinForm对象,但这对我们没什么用,因为我们想存储的是我们定义过的对象Form1的属性与方法,所以我们要做的是利用CType把WinForm对象转化为Form1对象:
Public ReadOnly Property MainForm() As Form1
Get
Return CType(Control.Parent, Form1)
End Get
End Property
创建一个线程
在VB.NET里创建线程非常容易,我们必须做的是实例化一个类(线程类)并找出这个类中的一个方法(该方法作为进入线程的入口点)。我们假设在类WordCounter和CharCounter里面的这个方法名为StartThreading。在类MyWorkThread定义中加入这个方法,特别要加上关键字MustOverride,这可以保证任何从MyWorkThread继承而来的类都必须包含它自己的关于StartThreading方法的定义(这对于C++开发者来说就是一个纯虚函数)。
Public MustOverride Sub StartThreading()
启动线程,我们要调用名为Go的方法,这个方法创建一个ThreadStart对象,然后创建一个线程对象并启动这个线程。
Public Sub Go()
'为了建立线程,首先创建ThreadStart
Dim start As ThreadStart
start = New ThreadStart(AddressOf Me.StartThreading)
' 初始化并启动线程
m_thread = New Thread(start)
m_thread.Start()
End Sub
关键字AddressOf返回一个StartThreading类的委托(C++管这叫指针),因为某些原因,你不能在一行内声明ThreadStart变量并同时给它赋值——它会抛出一个异常,一定要在单独的两行里完成这个工作。
当线程的工作完成后,我们假设要调用一个名为Finish的方法,在这篇文章里,这个方法的任务是设置文本框的文字为“Finished”,如果我们想的话还可以添加一些更酷的功能。
' Finish – 当线程停止处理时调用的一个方法
Public Sub Finish()
ControlText = "Finished!"
End Sub
事件
关于线程的一个奇怪的地方是线程的“事件”不同于一个VB事件,一个VB事件就是例如WinForm控件中的按钮的OnClick方法。这是因为线程源于传统的WIN32程序——它是没有事件的。一个线程的事件在当一个对象释放一个锁的时候被触发。所以,如果你的线程因为锁的原因被阻塞,它正在等待重获自由,在锁被解除后,一个事件触发了,你的线程得以终止阻塞状态并进入临界区(locked code)进行处理。
System.Threading名字空间包含一个名为ManualRaiseEvent的对象。这个对象啥也不做,它存在的唯一价值就是在两种阻塞状态中提供一种——“告知”或“未告知”。如果阻塞未告知,它就是“被阻塞的”,你不能进入它。如果是告知,你就能进(对C++开发者来说这相当于Win32 API的 CreateEvent调用)。
我们准备使用一个ManualRaiseEvent来控制我们的“暂停/重新开始”功能。我们的线程要进入一个无限循环。在循环开始时线程向主窗体请求文本框内文本的一个拷贝。它然后开始处理,返回结果后把自己“暂停”,一旦文本发生变化,所有的线程又都重新动起来,这个循环又重新开始。我们也可以要求取消线程,这种情况下它将会放弃无限循环并自然终止。
为了“暂停”线程,我们需要“reset” ManualRaiseEvent对象,这将会使这个线程进入未告知状态。
'通知一个线程暂停
Public Sub PauseThread()
m_pause.Reset()
End Sub
为了使暂停的线程“继续”,我们需要“set”对象,这将会使这个线程进入告知状态。
' 通知一个暂停的线程重新开始处理工作
Public Sub ResumeThread()
m_pause.Set()
End Sub
如果我们想取消线程,必须把是否取消线程的标志符设置为真,然后重新开始线程。
'终止线程
Public Sub Cancel()
' 设置标志符
m_iscancelled = True
' 重新开始线程
ResumeThread()
End Sub
很有必要在这里说一句,这些控制线程的命令不是那种“你现在就给我停止工作!”类型的命令,它们是“你的工作如果结束了你就停止吧”这种温和的命令。理想化地,在编写多线程代码时,采取的手段是让事情自然地终止,而不是去强迫它。
我们加上下面这段代码,可以知道线程是否被取消了。
Public Function IsCancelled() As Boolean
Return m_IsCancelled
End Function
ManualRaiseEvent对象里我们需要的最后一个重要的方法是IsPaused,这么称呼有点用词不当,因为如果线程被暂停,它没有任何返回值——它只是一个起阻塞作用的函数。我们接下来做的是返回IsCancelled的相反值(指对IsCancelled取非),它会告诉我们是“继续工作”还是“退出”,当这件事发生时,我们知道线程是否暂停了。如果暂停了,没有返回值,线程在取消暂停前将进入等待状态;如果没有被暂停,将返回IsCancelled的当前值。所以,如果线程被取消了,IsPaused将返回False;如果线程没有被取消,IsPaused将返回True。
' IsPaused ——通知一个线程继续...
Public Function IsPaused() As Boolean
' wait _disibledevent=>
现在我们进入无限循环。我们使用IsPaused来等待指示信号——是需要处理结果还是退出循环:
' 进入循环等待“处理开始”的请求
Do While True
' 我们要停止吗?
If IsPaused() = False Then
Exit Do
End If
下一步,执行工作任务并更新结果(我们会即时看到文本框的内容)。
Dim worktext As String = MainForm.WorkText
If worktext.Length = 1 Then
controltext = "1 char"
Else
controltext = worktext.Length.ToString & " chars"
End If
完成任务后,我们(指线程)把自己暂停。循环重新返回开始处并在IsPaused作用下进入阻塞状态,直到下一次我们被重新启动。
' 线程返回至等待状态
PauseThread()
Loop
最后,如果我们完成了工作,我们调用Finish。
'告诉它我们干完活了
Finish()
End Sub
End Class
创建wordCounter
WordCounter 实质上和 CharCounter是一样的:
Imports System
Imports System.WinForms
Imports System.Threading
Public Class WordCounter
Inherits MyWorkThread
Public Overrides Sub StartThreading()
' tell it we've started....
controltext = "Started!"
' go into a loop and wait until we're asked to start processing...
Do While True
' are we paused?
If IsPaused() = False Then
Exit Do
End If
' get hold of the text...
Dim worktext As String = MainForm.WorkText
' split the text into words...
Dim words() As String = worktext.split(" ".tochararray)
' report the number of words...
If words.Length = 1 Then
controltext = "1 word"
Else
controltext = words.Length & " words"
End If
' put the thread back into a wait state...
PauseThread()
Loop
' tell it we've finished...
Finish()
End Sub
End Class
线程支持
现在我们开始在窗体form1中创建我们的线程了。
在窗体Form1上创建这些成员变量:
' 跟踪线程轨迹
Const NUMTHREADS As Integer = 1
Dim m_ThreadOutputs(NUMTHREADS) As TextBox
Dim m_Threads(NUMTHREADS) As MyWorkThread
' create somewhere to keep hold of the text...
Dim m_WorkText As String
Dim m_WorkTextLock As New ReaderWriterLock()
System.Threading.ReaderWriterLock创建了我们讨论过的锁,为了使用锁,我们像这样创建一个属性:
' create a property that returns the text to process... we want to
' handle reader/writer locks properly...
Public Property WorkText() As String
Get
' lock the object for reading...
m_WorkTextLock.AcquireReaderLock(-1)
' get the value back, as we're sure no _disibledevent=>
' release the lock _disibledevent=>
' release the lock...
m_WorkTextLock.ReleaseWriterLock()
End Set
End Property
给AcquireReaderLock 和 AcquireWriterLock指定的“-1”是告诉对象我们会“一直等待”直到锁的解除。这种做法在多线程编码中是一种相当经典的方法,不这么很可能产生一些问题。当使用完被阻塞代码后进行解锁是极其重要的。举例来说,如果我们忘了释放“作者”锁,其它的锁也都打不开,一切线程都会慢慢停下来。“锁”和“解锁”命令的数目必须一致,如果锁了五次,就要再解锁五次。
这种做法的优点是透明地调用线程。举例来说,我们知道数据同步存取的复杂性,并把问题和解决办法区分开来。为了找出它将要处理的文本,我们十六个线程的每一个都会调用Form1.WorkText,如果因为m_WorkText的数据正处于更改的处理过程中而禁止对其进行访问,这个调用就会自动阻塞直到“作者”锁的主人完成处理工作。
为了创建线程,我们需要一个名为StartThreads的函数,它对我们的线程进行循环,根据线程的数量,创建WordCounter 或 CharCounter线程:
Sub StartThreads()
' 创建新线程
Dim n As Integer
For n = 0 To NUMTHREADS - 1
'创建一个线程
Select Case n Mod 2
Case 0
m_threads(n) = New WordCounter()
Case Else
m_threads(n) = New CharCounter()
End Select
一旦建立好线程,我们就告诉用哪个文本框显示结果并使线程启动:
' 配置并启动线程
m_threads(n).Control = m_ThreadOutputs(n)
m_threads(n).Go()
Next
End Sub
当然了,我们需要把StartThreads连接到cmdStartThreads按钮上:
Protected Sub cmdStartThreads_Click(ByVal sender As Object, _
ByVal e As System.EventArgs)
StartThreads()
End Sub
为了让线程对变化做出反应,我们需要响应TextChanged事件,首先,我们拷贝文本框的值到使用WorkText属性的成员变量m_WorkText,这可以保证在合适的时候加锁。例如这个值在有其它线程读取的时候不允许更改。
Public Sub txtText_TextChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles txtText.TextChanged
' 更新值
WorkText = txtText.Text
设置属性后,我们对所有线程进行循环并通知它们继续运行:
' now, resume all of our threads...
Dim n As Integer
For n = 0 To NUMTHREADS - 1
If Not m_threads(n) Is Nothing Then m_threads(n).ResumeThread()
Next
End Sub
好啦!现在你点击StartThreads按钮和更改文本框中的文字,线程将会处理文字并返回结果。
关闭线程
关闭线程时和创建它们的时候处理方法差不多:
Sub StopThreads()
'循环访问进行停止
Dim n As Integer
For n = 0 To NUMTHREADS - 1
'是否还有线程?
If Not m_threads(n) Is Nothing Then
' 通知线程cancel
m_threads(n).Cancel()
' 现在,等待线程终止
m_threads(n).WaitUntilFinished()
' 最后,移除线程
m_threads(n) = Nothing
End If
Next
End Sub
为了系统的纯净,当应用程序关闭时调用StopThreads:
Public Sub ThreadForm_Closing(ByVal sender As Object, _
ByVal e As System.ComponentModel.CancelEventArgs) Handles Form1.Closing
StopThreads()
End Sub
结论
这篇文章介绍了在.net中的System.Threading名字空间使用线程的方法,并列举了一个同步应用的例子,描述了多线程如何在同一块数据内工作。最后,请记住本文虽然用VB描述,但是它的手法和用C#开发没有什么区别。
Tags: 

延伸阅读

最新评论

发表评论