演练:用 Visual Basic 创作简单的多线程组件

BackgroundWorker 组件取代了 System.Threading 命名空间并添加了功能;但是,可以选择保留 System.Threading 命名空间以实现向后兼容并供将来使用。 有关更多信息,请参见 BackgroundWorker 组件概述

您可以编写能同时执行多个任务的应用程序。 这种能力称为“多线程处理”或“自由线程处理”,是设计占用处理器资源并且要求用户进行输入的组件的一种有效方式。 计算工资表信息的组件就是一个可能利用多线程处理的组件示例。 该组件可以在一个线程上处理用户输入到数据库的数据,而在另一个线程上执行频繁使用处理器的工资表计算。 通过在不同的线程上运行这些进程,用户不必等到计算机完成计算,就可以输入其他数据。 在本演练中,您将创建一个简单的多线程组件,该组件同时执行多个复杂的计算。

创建项目

您的应用程序由单个窗体和一个组件构成。 用户将输入值并指示该组件开始计算。 然后,窗体接收来自组件的值,并在标签控件中显示这些值。 组件执行需要大量使用处理器的计算,并在完成计算时通知窗体。 您将在组件中创建公共变量,用以保存从用户界面收到的值。 另外还要在组件中实现一些方法,根据这些变量的值执行计算。

备注

尽管函数对于计算值的方法通常是首选,但是不能在线程间传递参数,也不能返回值。有很多向线程提供值和从线程接收值的简单方法。在本演示中,您将通过更新公共变量将值返回到用户界面,当线程执行完毕后,使用事件通知主程序。

显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您现用的设置或版本。若要更改设置,请在“工具”菜单上选择“导入和导出设置”。有关更多信息,请参见 Visual Studio 设置

创建窗体

  1. 创建新的**“Windows 应用程序”**项目。

  2. 将应用程序命名为 Calculations,将 Form1.vb 重命名为 frmCalculations.vb。

  3. 当 Visual Studio 提示您重命名 Form1 代码元素时,请单击**“是”**。

    该窗体将用作应用程序的主用户界面。

  4. 向窗体中添加五个 Label 控件、四个 Button 控件和一个 TextBox 控件。

    控件

    名称

    Text

    Label1

    lblFactorial1

    (空白)

    Label2

    lblFactorial2

    (空白)

    Label3

    lblAddTwo

    (空白)

    Label4

    lblRunLoops

    (空白)

    Label5

    lblTotalCalculations

    (空白)

    Button1

    btnFactorial1

    Factorial

    Button2

    btnFactorial2

    Factorial - 1

    Button3

    btnAddTwo

    Add Two

    Button4

    btnRunLoops

    Run a Loop

    TextBox1

    txtValue

    (空白)

创建 Calculator 组件

  1. 从**“项目”菜单中选择“添加组件”**。

  2. 将此组件命名为 Calculator。

向 Calculator 组件添加公共变量

  1. 打开 Calculator 的**“代码编辑器”**。

  2. 添加创建公共变量的语句,这些变量用于将值从 frmCalculations 传递给每个线程。

    变量 varTotalCalculations 将保留该组件执行的计算总次数的累计值,而其他变量将接收来自窗体的值。

    Public varAddTwo As Integer
    Public varFact1 As Integer
    Public varFact2 As Integer
    Public varLoopValue As Integer
    Public varTotalCalculations As Double = 0
    

向 Calculator 组件添加方法和事件

  1. 声明事件,组件将使用这些事件将值传送到窗体。 紧接着上一步输入的变量声明的下方,键入下列代码:

    Public Event FactorialComplete(ByVal Factorial As Double, ByVal _
       TotalCalculations As Double)
    Public Event FactorialMinusComplete(ByVal Factorial As Double, ByVal _
       TotalCalculations As Double)
    Public Event AddTwoComplete(ByVal Result As Integer, ByVal _
       TotalCalculations As Double)
    Public Event LoopComplete(ByVal TotalCalculations As Double, ByVal _
       Counter As Integer)
    
  2. 在紧接着步骤 1 中输入的变量声明的下方,键入以下代码:

    ' This sub will calculate the value of a number minus 1 factorial 
    ' (varFact2-1!).
    Public Sub FactorialMinusOne()
       Dim varX As Integer = 1
       Dim varTotalAsOfNow As Double
       Dim varResult As Double = 1
       ' Performs a factorial calculation on varFact2 - 1.
       For varX = 1 to varFact2 - 1
          varResult *= varX
          ' Increments varTotalCalculations and keeps track of the current
          ' total as of this instant.
          varTotalCalculations += 1
          varTotalAsOfNow = varTotalCalculations
       Next varX
       ' Signals that the method has completed, and communicates the 
       ' result and a value of total calculations performed up to this 
       ' point
       RaiseEvent FactorialMinusComplete(varResult, varTotalAsOfNow)
    End Sub
    
    ' This sub will calculate the value of a number factorial (varFact1!).
    Public Sub Factorial()
       Dim varX As Integer = 1
       Dim varResult As Double = 1
       Dim varTotalAsOfNow As Double = 0
       For varX = 1 to varFact1
           varResult *= varX
           varTotalCalculations += 1
           varTotalAsOfNow = varTotalCalculations
       Next varX
       RaiseEvent FactorialComplete(varResult, varTotalAsOfNow)
    End Sub
    
    ' This sub will add two to a number (varAddTwo + 2).
    Public Sub AddTwo()
       Dim varResult As Integer
       Dim varTotalAsOfNow As Double
       varResult = varAddTwo + 2
       varTotalCalculations += 1
       varTotalAsOfNow = varTotalCalculations
       RaiseEvent AddTwoComplete(varResult, varTotalAsOfNow)
    End Sub
    
    ' This method will run a loop with a nested loop varLoopValue times.
    Public Sub RunALoop()
       Dim varX As Integer
       Dim varY As Integer
       Dim varTotalAsOfNow As Double
       For varX = 1 To varLoopValue
          ' This nested loop is added solely for the purpose of slowing
          ' down the program and creating a processor-intensive
          ' application.
          For varY = 1 To 500
             varTotalCalculations += 1
             varTotalAsOfNow = varTotalCalculations
          Next
       Next
       RaiseEvent LoopComplete(varTotalAsOfNow, varX - 1)
    End Sub
    

将用户输入传输到组件

下一步是向 frmCalculations 中添加代码,以接收用户输入以及从 Calculator 组件接收值和向它传送值。

实现 frmCalculations 的前端功能

  1. 从**“生成”菜单中选择“生成解决方案”**。

  2. 在 Windows 窗体设计器中打开 frmCalculations。

  3. 在**“工具箱”中找到“计算组件”选项卡。 将一个“Calculator”**组件拖动到设计图面上。

  4. 在**“属性”窗口中单击“事件”**按钮。

  5. 双击四个事件中的每一个,在 frmCalculations 中创建事件处理程序。 在每个事件处理程序都创建好以后,您需要返回到设计器。

  6. 插入下面的代码,以处理窗体将从 Calculator1 接收的事件:

    Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete
        lblAddTwo.Text = Result.ToString
        btnAddTwo.Enabled = True
        lblTotalCalculations.Text = "TotalCalculations are " & _
            TotalCalculations.ToString
    End Sub
    
    Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete
        ' Displays the returned value in the appropriate label.
        lblFactorial1.Text = Factorial.ToString
        ' Re-enables the button so it can be used again.
        btnFactorial1.Enabled = True
        ' Updates the label that displays the total calculations performed
        lblTotalCalculations.Text = "TotalCalculations are " & _
           TotalCalculations.ToString
    End Sub
    
    Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete
        lblFactorial2.Text = Factorial.ToString
        btnFactorial2.Enabled = True
        lblTotalCalculations.Text = "TotalCalculations are " & _
            TotalCalculations.ToString
    End Sub
    
    Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete
        btnRunLoops.Enabled = True
        lblRunLoops.Text = Counter.ToString
        lblTotalCalculations.Text = "TotalCalculations are " & _
           TotalCalculations.ToString
    End Sub
    
  7. 在**“代码编辑器”**的底部找到 End Class 语句。 紧接着其上方,添加下列代码以处理按钮单击:

    Private Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnFactorial1.Click
       ' Passes the value typed in the txtValue to Calculator.varFact1.
       Calculator1.varFact1 = CInt(txtValue.Text)
       ' Disables the btnFactorial1 until this calculation is complete.
       btnFactorial1.Enabled = False
       Calculator1.Factorial()
    End Sub
    
    Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e _
       As System.EventArgs) Handles btnFactorial2.Click
       Calculator1.varFact2 = CInt(txtValue.Text)
       btnFactorial2.Enabled = False
       Calculator1.FactorialMinusOne()
    End Sub
    
    Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnAddTwo.Click
       Calculator1.varAddTwo = CInt(txtValue.Text)
       btnAddTwo.Enabled = False
       Calculator1.AddTwo()
    End Sub
    
    Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnRunLoops.Click
       Calculator1.varLoopValue = CInt(txtValue.Text)
       btnRunLoops.Enabled = False
       ' Lets the user know that a loop is running.
       lblRunLoops.Text = "Looping"
       Calculator1.RunALoop()
    End Sub
    

测试应用程序

现在项目已创建完毕,它将能够执行若干复杂计算的组件与窗体结合在一起。 虽然尚未实现多线程处理功能,但在继续进行之前,您将测试项目以验证其功能。

测试项目

  1. 从**“调试”菜单中选择“启动调试”**。 应用程序开始运行,出现 frmCalculations。

  2. 在文本框中键入 4,然后单击标记为**“Add Two”**的按钮。

    按钮下方的标签中应该显示数字“6”,lblTotalCalculations 中应该显示“Total Calculations are 1”。

  3. 现在单击标记为**“Factorial - 1”**的按钮。

    该按钮的下方应显示数字“6”,而 lblTotalCalculations 中现在应显示“Total Calculations are 4”。

  4. 将文本框中的值更改为 20,然后单击标记为**“Factorial”**的按钮。

    该按钮的下方显示数字“2.43290200817664E+18”,而 lblTotalCalculations 中现在显示为“Total Calculations are 24”。

  5. 将文本框中的值更改为 50000,然后单击标记为**“Run A Loop”**的按钮。

    注意,在该按钮重新启用之前存在一个很短但可察觉到的间隔。 此按钮下的标签应显示“50000”,而总的计算次数显示为“25000024”。

  6. 将文本框中的值更改为 5000000,单击标记为**“Run A Loop”的按钮,紧接着再单击标记为“Add Two”的按钮。 再次单击“Add Two”**。

    直到循环已经完成,该按钮以及窗体上的任何控件才有响应。

    如果程序只运行单个执行线程,则类似上述示例的频繁使用处理器的计算倾向于占用该程序,直到计算已经完成。 在下一节中,您将向应用程序添加多线程处理功能,以便一次可以运行多个线程。

添加多线程处理功能

上面的示例演示了只运行单个执行线程的应用程序的限制。 在下一节中,您将使用 Thread 类向组件添加多个执行线程。

添加 Threads 子例程

  1. 在**“代码编辑器”中打开“Calculator.vb”**。 在代码顶部附近,找到 Public Class Calculator 行。 紧接着它的下方,键入下列代码:

    ' Declares the variables you will use to hold your thread objects.
    Public FactorialThread As System.Threading.Thread
    Public FactorialMinusOneThread As System.Threading.Thread
    Public AddTwoThread As System.Threading.Thread
    Public LoopThread As System.Threading.Thread
    
  2. 在紧挨着代码底部的 End Class 语句的前面,添加以下方法:

    Public Sub ChooseThreads(ByVal threadNumber As Integer)
    ' Determines which thread to start based on the value it receives.
       Select Case threadNumber
          Case 1
             ' Sets the thread using the AddressOf the subroutine where
             ' the thread will start.
             FactorialThread = New System.Threading.Thread(AddressOf _
                Factorial)
             ' Starts the thread.
             FactorialThread.Start()
          Case 2
             FactorialMinusOneThread = New _
                System.Threading.Thread(AddressOf FactorialMinusOne)
             FactorialMinusOneThread.Start()
          Case 3
             AddTwoThread = New System.Threading.Thread(AddressOf AddTwo)
             AddTwoThread.Start()
          Case 4
             LoopThread = New System.Threading.Thread(AddressOf RunALoop)
             LoopThread.Start()
       End Select
    End Sub
    

    Thread 对象实例化时,它要求一个 ThreadStart 对象形式的参数。 ThreadStart 对象是指向子例程地址的委托,线程从该地址开始。 ThreadStart 对象不能带参数或传递值,因此不能表示函数。 AddressOf 运算符 (Visual Basic) 返回一个用作 ThreadStart 对象的委托。 刚刚实现的 ChooseThreads 子例程将从调用它的程序接收值,并使用该值来确定要启动的适当线程。

将线程启动代码添加到 frmCalculations

  1. 在**“代码编辑器”中打开“frmCalculations.vb”**文件。 找到 Sub btnFactorial1_Click。

    1. 注释掉直接调用方法。Calculator1.Factorial显示的行:

      ' Calculator1.Factorial
      
    2. 添加下面的行,以调用 Calculator1.ChooseThreads 方法:

      ' Passes the value 1 to Calculator1, thus directing it to start the ' correct thread.
      Calculator1.ChooseThreads(1)
      
  2. 对其他 button_click 子例程做类似的修改。

    备注

    请务必包括 threads 参数的适当值。

    完成后,代码看起来应该类似下面这样:

    Private Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnFactorial1.Click
       ' Passes the value typed in the txtValue to Calculator.varFact1.
       Calculator1.varFact1 = CInt(txtValue.Text)
       ' Disables the btnFactorial1 until this calculation is complete.
       btnFactorial1.Enabled = False
       ' Calculator1.Factorial()
       ' Passes the value 1 to Calculator1, thus directing it to start the
       ' Correct thread.
       Calculator1.ChooseThreads(1)
    End Sub
    
    Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnFactorial2.Click
       Calculator1.varFact2 = CInt(txtValue.Text)
       btnFactorial2.Enabled = False
       ' Calculator1.FactorialMinusOne()
       Calculator1.ChooseThreads(2)
    End Sub
    
    Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnAddTwo.Click
       Calculator1.varAddTwo = CInt(txtValue.Text)
       btnAddTwo.Enabled = False
       ' Calculator1.AddTwo()
       Calculator1.ChooseThreads(3)
    End Sub
    
    Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _
       System.EventArgs) Handles btnRunLoops.Click
       Calculator1.varLoopValue = CInt(txtValue.Text)
       btnRunLoops.Enabled = False
       ' Lets the user know that a loop is running.
       lblRunLoops.Text = "Looping"
       ' Calculator1.RunALoop()
       Calculator1.ChooseThreads(4)
    End Sub
    

封送对控件的调用

现在您将加速窗体的显示更新。 由于控件总是由主执行线程所有,因此从从属线程中调用任何控件都需要“封送处理”调用。 封送就是跨越线程边界移动调用的行为,需要消耗大量的资源。 为了使需要发生的封送处理量减到最少,并确保以线程安全的方式处理调用,应使用 BeginInvoke 来调用主执行线程上的方法,从而使必须发生的跨线程边界的封送处理量减到最少。 当调用操作控件的方法时,必须使用这种调用。 有关更多信息,请参见 如何:从线程中操作控件

创建控件调用过程

  1. 打开 frmCalculations 的**“代码编辑器”**。 在声明部分,添加下列代码。

    Public Delegate Sub FHandler(ByVal Value As Double, ByVal _
       Calculations As Double)
    Public Delegate Sub A2Handler(ByVal Value As Integer, ByVal _
       Calculations As Double)
    Public Delegate Sub LDhandler(ByVal Calculations As Double, ByVal _
       Count As Integer)
    

    InvokeBeginInvoke 需要将适当方法的委托作为参数。 这些代码行声明一些委托签名,这些签名将被 BeginInvoke 用来调用适当的方法。

  2. 在代码中添加下列空方法。

    Public Sub FactHandler(ByVal Factorial As Double, ByVal TotalCalculations As _
       Double)
    End Sub
    Public Sub Fact1Handler(ByVal Factorial As Double, ByVal TotalCalculations As _
       Double)
    End Sub
    Public Sub Add2Handler(ByVal Result As Integer, ByVal TotalCalculations As _
       Double)
    End Sub
    Public Sub LDoneHandler(ByVal TotalCalculations As Double, ByVal Counter As _
       Integer)
    End Sub
    
  3. 在**“编辑”菜单中,使用“剪切”“粘贴”**,将 Sub Calculator1_FactorialComplete 中的所有代码剪切并粘贴到 FactHandler 中。

  4. 对 Calculator1_FactorialMinusComplete 和 Fact1Handler、Calculator1_AddTwoComplete 和 Add2Handler 以及 Calculator1_LoopComplete 和 LDoneHandler 重复上面的步骤。

    完成后,Calculator1_FactorialComplete、Calculator1_FactorialMinusComplete、Calculator1_AddTwoComplete 和 Calculator1_LoopComplete 中应该没有剩余的代码,它们以前包含的所有代码都应该已经移到适当的新方法中。

  5. 调用 BeginInvoke 方法以异步调用这些方法。 您可以从窗体 (me) 或该窗体上的任何控件调用 BeginInvoke

    Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete
       ' BeginInvoke causes asynchronous execution to begin at the address
       ' specified by the delegate. Simply put, it transfers execution of 
       ' this method back to the main thread. Any parameters required by 
       ' the method contained at the delegate are wrapped in an object and 
       ' passed. 
       Me.BeginInvoke(New FHandler(AddressOf FactHandler), New Object() _
          {Factorial, TotalCalculations })
    End Sub
    
    Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete
       Me.BeginInvoke(New FHandler(AddressOf Fact1Handler), New Object() _
          { Factorial, TotalCalculations })
    End Sub
    
    Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete
       Me.BeginInvoke(New A2Handler(AddressOf Add2Handler), New Object() _
          { Result, TotalCalculations })
    End Sub
    
    Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete
       Me.BeginInvoke(New LDHandler(AddressOf Ldonehandler), New Object() _
          { TotalCalculations, Counter })
    End Sub
    

    看起来似乎事件处理程序仅仅是对下一个方法进行调用。 实际上,该事件处理程序实现了在主操作线程上调用方法。 这种方法可节省跨线程边界的调用,并使多线程应用程序能够有效运行而不必担心导致死锁。 有关在多线程环境中使用控件的详细信息,请参见 如何:从线程中操作控件

  6. 保存您的工作。

  7. 通过从**“调试”菜单中选择“启动调试”**来测试您的解决方案。

    1. 在文本框内键入 10000000 并单击**“Run A Loop”**。

      此按钮下方的标签中显示“Looping”。 运行这个循环应该占用很长时间。 如果它完成得太快,请相应地调整该数字的大小。

    2. 连续地快速单击仍在启用的三个按钮。 您会发现所有按钮都响应您的输入。 **“Add Two”下方的标签应该第一个显示结果。 结果稍后将显示在阶乘按钮下方的标签中。 估计这些结果会无限大,因为 10,000,000 的阶乘返回的数字对于双精度变量而言太大,以至超出了它包含的范围。 最后,再过片刻,结果将返回到“Run A Loop”**按钮的下方。

      正如刚刚观察到的,在四个单独的线程上同时执行四组独立的计算。 用户界面保持对输入的响应,并在每个线程完成后返回结果。

协调线程

有经验的多线程应用程序用户可能会发现已键入的代码中存在细微缺陷。 从 Calculator 中每个执行计算的子例程撤回以下代码行:

varTotalCalculations += 1
varTotalAsOfNow = varTotalCalculations

这两行代码递增公共变量 varTotalCalculations 并将局部变量 varTotalAsOfNow 设置为该值。 然后,该值返回给 frmCalculations,并显示在标签控件中。 但是否正在返回正确的值? 如果只有单个执行线程在运行,则答案明显是正确的。 但是如果有多个线程在运行,答案则变得不太确定。 每个线程都具有增加变量 varTotalCalculations 的能力。 有可能出现这样的情况:在一个线程增加该变量之后和将该值复制到 varTotalAsOfNow 之前的间隔里,另一个线程可能通过增加该变量而更改它的值。 这将导致有可能每个线程实际上在报告不正确的结果。 Visual Basic 提供 SyncLock 语句 来实现线程同步,从而确保每个线程始终返回准确的结果。 SyncLock 的语法如下所示:

SyncLock AnObject
   Insert code that affects the object
   Insert some more
   Insert even more
' Release the lock
End SyncLock

输入 SyncLock 块后,在指定的线程对所讨论的对象拥有专用锁之前,对指定表达式的执行一直被堵塞。 在上面显示的示例中,对 AnObject 的执行将会阻塞。 必须对返回引用(而非值)的对象使用 SyncLock。 然后,执行以块的形式继续进行,不会受到其他线程的干扰。 作为一个单元执行的语句集称为“原子”。 当遇到 End SyncLock 时,表达式将被释放,线程将可以继续正常工作。

将 SyncLock 语句添加到应用程序

  1. 在**“代码编辑器”中打开“Calculator.vb”**。

  2. 找到下列代码的每个实例:

    varTotalCalculations += 1
    varTotalAsOfNow = varTotalCalculations
    

    应该有此代码的四个实例,每个计算方法中有一个。

  3. 修改此代码,使其显示为如下形式:

    SyncLock Me
       varTotalCalculations += 1
       varTotalAsOfNow = varTotalCalculations
    End SyncLock
    
  4. 保存工作,并按上例所示进行测试。

    您可能注意到对程序性能的细微影响。 这是因为当组件获得排他锁后,线程的执行停止。 尽管此方法确保了准确性,但是也影响了多线程的某些性能优点。 应该认真考虑锁定线程的必要性,并且仅当绝对必要时才予以实现。

请参见

任务

如何:协调多个执行线程

演练:使用 Visual C# 创作简单的多线程组件

参考

BackgroundWorker

概念

基于事件的异步模式概述

其他资源

使用组件编程

组件编程演练

组件中的多线程处理