Bagikan melalui


Walkthrough: Authoring a Simple Multithreaded Component with Visual Basic

The BackgroundWorker component replaces and adds functionality to the System.Threading namespace; however, the System.Threading namespace is retained for both backward compatibility and future use, if you choose. For more information, see BackgroundWorker Component Overview.

You can write applications that are able to perform multiple tasks simultaneously. This ability, called multithreading, or free threading, is a powerful way to design components that are processor-intensive and require user input. An example of a component that might make use of multithreading would be a component that calculates payroll information. The component could process data entered into a database by a user on one thread while processor-intensive payroll calculations were performed on another. By running these processes on separate threads, users do not need to wait for the computer to complete calculations before entering additional data. In this walkthrough, you will create a simple multithreaded component that performs multiple complex calculations simultaneously.

Creating the Project

Your application will consist of a single form and a component. The user will input values and signal to the component to begin calculations. The form will then receive values from your component and display them in label controls. The component will perform the processor-intensive calculations and signal the form when complete. You will create public variables in your component to hold values received from your user interface. You will also implement methods in your component to perform the calculations based on the values of these variables.

Note

While a function is usually preferable for a method that calculates a value, arguments cannot be passed between threads, nor can values be returned. There are many simple ways to supply values to threads and to receive values from them. In this demonstration, you will return values to your user interface by updating public variables, and events will be used to notify the main program when a thread has completed execution.

The dialog boxes and menu commands you see might differ from those described in Help depending on your active settings or edition. To change your settings, choose Import and Export Settings on the Tools menu. For more information, see Visual Studio Settings.

To create the form

  1. Create a new Windows Application project.

  2. Name the application Calculations and rename Form1.vb as frmCalculations.vb.

  3. When Visual Studio prompts you to rename the Form1 code element, click Yes.

    This form will serve as the primary user interface for your application.

  4. Add five Label controls, four Button controls, and one TextBox control to your form.

    Control

    Name

    Text

    Label1

    lblFactorial1

    (blank)

    Label2

    lblFactorial2

    (blank)

    Label3

    lblAddTwo

    (blank)

    Label4

    lblRunLoops

    (blank)

    Label5

    lblTotalCalculations

    (blank)

    Button1

    btnFactorial1

    Factorial

    Button2

    btnFactorial2

    Factorial - 1

    Button3

    btnAddTwo

    Add Two

    Button4

    btnRunLoops

    Run a Loop

    TextBox1

    txtValue

    (blank)

To create the Calculator component

  1. From the Project menu, select Add Component.

  2. Name this component Calculator.

To add public variables to the Calculator component

  1. Open the Code Editor for Calculator.

  2. Add statements to create public variables that you will use to pass values from frmCalculations to each thread.

    The variable varTotalCalculations will keep a running total of the total number of calculations performed by the component, and the other variables will receive values from the form.

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

To add methods and events to the Calculator component

  1. Declare the events that your component will use to communicate values to your form. Immediately beneath the variable declarations entered in the previous step, type the following code:

    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. Immediately beneath the variable declarations entered in step 1, type the following code:

    ' 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
    

Transferring User Input to the Component

The next step is to add code to frmCalculations to receive input from the user and to transfer and receive values to and from the Calculator component.

To implement front-end functionality to frmCalculations

  1. Select Build Solution from the Build menu.

  2. Open frmCalculations in the Windows Forms Designer.

  3. Locate the Calculations Components tab in the Toolbox. Drag a Calculator component onto the design surface.

  4. In the Properties windows, click the Events button.

  5. Double-click each of the four events to create event handlers in frmCalculations. You will need to return to the designer after each event handler is created.

  6. Insert the following code to handle the events your form will receive from 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. Locate the End Class statement at the bottom of the Code Editor. Immediately above it, add the following code to handle button clicks:

    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
    

Testing Your Application

You have now created a project that incorporates a form and a component capable of performing several complex calculations. Though you have not implemented multithreading capability yet, you will test your project to verify its functionality before proceeding.

To test your project

  1. From the Debug menu, choose Start Debugging. The application begins and frmCalculations appears.

  2. In the text box, type 4, then click the button labeled Add Two.

    The numeral "6" should be displayed in the label beneath it, and "Total Calculations are 1" should be displayed in lblTotalCalculations.

  3. Now click the button labeled Factorial - 1.

    The numeral "6" should be displayed beneath the button, and lblTotalCalculations now reads "Total Calculations are 4."

  4. Change the value in the text box to 20, then click the button labeled Factorial.

    The number "2.43290200817664E+18" is displayed beneath it, and lblTotalCalculations now reads "Total Calculations are 24."

  5. Change the value in the text box to 50000, and then click the button labeled Run A Loop.

    Note that there is a small but noticeable interval before this button is re-enabled. The label under this button should read "50000" and the total calculations displayed are "25000024."

  6. Change the value in the text box to 5000000 and click the button labeled Run A Loop, and then immediately click the button labeled Add Two. Click Add Two again.

    The button does not respond, nor will any control on the form respond until the loops have been completed.

    If your program runs only a single thread of execution, processor-intensive calculations such as the above example have the tendency to tie up the program until the calculations have been completed. In the next section, you will add multithreading capability to your application so that multiple threads may be run at once.

Adding Multithreading Capability

The previous example demonstrated the limitations of applications that run only a single thread of execution. In the next section you will use the Thread class to add multiple threads of execution to your component.

To add the Threads subroutine

  1. Open Calculator.vb in the Code Editor. Near the top of the code, locate the Public Class Calculator line. Immediately beneath it, type the following:

    ' 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. Immediately before the End Class statement at the bottom of the code, add the following method:

    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
    

    When a Thread object is instantiated, it requires an argument in the form of a ThreadStart object. The ThreadStart object is a delegate that points to the address of the subroutine where the thread is to begin. A ThreadStart object cannot take parameters or pass values, and thus cannot indicate a function. The AddressOf Operator (Visual Basic) returns a delegate that serves as the ThreadStart object. The ChooseThreads sub you just implemented will receive a value from the program calling it and use that value to determine the appropriate thread to start.

To add the thread-starting code to frmCalculations

  1. Open the frmCalculations.vb file in the Code Editor. Locate Sub btnFactorial1_Click.

    1. Comment out the line that calls the Calculator1.Factorialmethod directly as shown:

      ' Calculator1.Factorial
      
    2. Add the following line to call the Calculator1.ChooseThreads method:

      ' Passes the value 1 to Calculator1, thus directing it to start the ' correct thread.
      Calculator1.ChooseThreads(1)
      
  2. Make similar modifications to the other button_click subroutines.

    Note

    Be sure to include the appropriate value for the threads argument.

    When you have finished, your code should look similar to the following:

    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
    

Marshaling Calls to Controls

You will now facilitate updating the display on the form. Because controls are always owned by the main thread of execution, any call to a control from a subordinate thread requires a marshaling call. Marshaling is the act of moving a call across thread boundaries, and is very expensive in terms of resources. To minimize the amount of marshaling that needs to occur, and to be sure that your calls are handled in a thread-safe manner, you will use the BeginInvoke to invoke methods on the main thread of execution, thereby minimizing the amount of cross-thread-boundary marshaling that must occur. This kind of call is necessary when calling methods that manipulate controls. For more information, see How to: Manipulate Controls from Threads.

To create the control-invoking procedures

  1. Open the Code Editor for frmCalculations. In the declarations section, add the following code.

    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)
    

    Invoke and BeginInvoke require a delegate to the appropriate method as an argument. These lines declare the delegate signatures that will be used by BeginInvoke to invoke the appropriate methods.

  2. Add the following empty methods to your code.

    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. From the Edit menu, use Cut and Paste to cut all the code from Sub Calculator1_FactorialComplete and paste it into FactHandler.

  4. Repeat the previous step for Calculator1_FactorialMinusComplete and Fact1Handler, Calculator1_AddTwoComplete and Add2Handler, and Calculator1_LoopComplete and LDoneHandler.

    When finished, there should be no code remaining in Calculator1_FactorialComplete, Calculator1_FactorialMinusComplete, Calculator1_AddTwoComplete, and Calculator1_LoopComplete, and all the code these used to contain should have been moved to the appropriate new methods.

  5. Call the BeginInvoke method to invoke the methods asynchronously. You can call BeginInvoke from either your form (me) or any of the controls on the form:

    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
    

    It may seem as though the event handler is simply making a call to the next method. The event handler is actually causing a method to be invoked on the main thread of operation. This approach saves on calls across thread boundaries and allows your multithreaded applications to run efficiently and without fear of causing lockup. For details on working with controls in a multithreaded environment, see How to: Manipulate Controls from Threads.

  6. Save your work.

  7. Test your solution by choosing Start Debugging from the Debug menu.

    1. Type 10000000 in the text box and click Run A Loop.

      "Looping" is displayed in the label beneath this button. This loop should take a significant amount of time to run. If it completes too early, adjust the size of the number accordingly.

    2. In rapid succession, click all three buttons that are still enabled. You will find that all buttons respond to your input. The label beneath Add Two should be the first to display a result. Results will later be displayed in the labels beneath the factorial buttons. These results evaluate to infinity, as the number returned by a 10,000,000 factorial is too large for a double-precision variable to contain. Finally, after an additional delay, results are returned beneath the Run A Loop button.

      As you just observed, four separate sets of calculations were performed simultaneously upon four separate threads. The user interface remained responsive to input, and results were returned after each thread completed.

Coordinating Your Threads

An experienced user of multithreaded applications may perceive a subtle flaw with the code as typed. Recall the lines of code from each calculation-performing subroutine in Calculator:

varTotalCalculations += 1
varTotalAsOfNow = varTotalCalculations

These two lines of code increment the public variable varTotalCalculations and set the local variable varTotalAsOfNow to this value. This value is then returned to frmCalculations and displayed in a label control. But is the correct value being returned? If only a single thread of execution is running, the answer is clearly yes. But if multiple threads are running, the answer becomes more uncertain. Each thread has the ability to increment the variable varTotalCalculations. It is possible that after one thread increments this variable, but before it copies the value to varTotalAsOfNow, another thread could alter the value of this variable by incrementing it. This leads to the possibility that each thread is, in fact, reporting inaccurate results. Visual Basic provides the SyncLock Statement to allow synchronization of threads to ensure that each thread always returns an accurate result. The syntax for SyncLock is as follows:

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

When the SyncLock block is entered, execution on the specified expression is blocked until the specified thread has an exclusive lock on the object in question. In the example shown above, execution is blocked on AnObject. SyncLock must be used with an object that returns a reference rather than a value. The execution may then proceed as a block without interference from other threads. A set of statements that execute as a unit are said to be atomic. When End SyncLock is encountered, the expression is freed and the threads are allowed to proceed normally.

To add the SyncLock statement to your application

  1. Open Calculator.vb in the Code Editor.

  2. Locate each instance of the following code:

    varTotalCalculations += 1
    varTotalAsOfNow = varTotalCalculations
    

    There should be four instances of this code, one in each calculation method.

  3. Modify this code so that it reads as follows:

    SyncLock Me
       varTotalCalculations += 1
       varTotalAsOfNow = varTotalCalculations
    End SyncLock
    
  4. Save your work and test it as in the previous example.

    You may notice a slight impact on the performance of your program. This is because execution of threads stops when an exclusive lock is obtained on your component. Although it ensures accuracy, this approach impedes some of the performance benefit of multiple threads. You should carefully consider the need for locking threads, and implement them only when absolutely necessary.

See Also

Tasks

How to: Coordinate Multiple Threads of Execution

Walkthrough: Authoring a Simple Multithreaded Component with Visual C#

Reference

BackgroundWorker

Concepts

Event-based Asynchronous Pattern Overview

Other Resources

Programming with Components

Component Programming Walkthroughs

Multithreading in Components