VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > VB.net教程 >
  • VB.net多线程入门

VB.net多线程入门
2011年06月03日
  出处 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变量并同时给它赋值――它会抛出一个异常,一定要在单独的两行里完成这个工作。

相关教程