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
Create a new Windows Application project.
Name the application
Calculations
and renameForm1.vb
asfrmCalculations.vb
.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.
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
From the Project menu, select Add Component.
Name this component
Calculator
.
To add public variables to the Calculator component
Open the Code Editor for
Calculator
.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
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)
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
Select Build Solution from the Build menu.
Open
frmCalculations
in the Windows Forms Designer.Locate the Calculations Components tab in the Toolbox. Drag a Calculator component onto the design surface.
In the Properties windows, click the Events button.
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.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
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
From the Debug menu, choose Start Debugging. The application begins and
frmCalculations
appears.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
.Now click the button labeled Factorial - 1.
The numeral "6" should be displayed beneath the button, and lblTotalCalculations now reads "Total Calculations are 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."
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."
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
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
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 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
Open the frmCalculations.vb file in the Code Editor. Locate
Sub btnFactorial1_Click
.Comment out the line that calls the
Calculator1.Factorial
method directly as shown:' Calculator1.Factorial
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)
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
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.
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
From the Edit menu, use Cut and Paste to cut all the code from
Sub Calculator1_FactorialComplete
and paste it intoFactHandler
.Repeat the previous step for
Calculator1_FactorialMinusComplete
andFact1Handler
,Calculator1_AddTwoComplete
andAdd2Handler
, andCalculator1_LoopComplete
andLDoneHandler
.When finished, there should be no code remaining in
Calculator1_FactorialComplete
,Calculator1_FactorialMinusComplete
,Calculator1_AddTwoComplete
, andCalculator1_LoopComplete
, and all the code these used to contain should have been moved to the appropriate new methods.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.
Save your work.
Test your solution by choosing Start Debugging from the Debug menu.
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.
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
Open Calculator.vb in the Code Editor.
Locate each instance of the following code:
varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations
There should be four instances of this code, one in each calculation method.
Modify this code so that it reads as follows:
SyncLock Me varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations End SyncLock
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
Concepts
Event-based Asynchronous Pattern Overview
Other Resources
Programming with Components
Component Programming Walkthroughs
Multithreading in Components