You would like to perform some involved background data processing but keep the user interface for your application responsive to user interaction.
Sample code folder: Chapter 14\UsingThreads
Use a BackgroundWorker control (or class) to manage the interaction between the main process and a worker thread.
This recipe's sample code starts a background worker thread that does some work, reporting its progress back to the main thread on a regular basis. The main thread has the option to cancel the worker thread. Create a new Windows Forms application, and add the following controls to Form1:
Arrange the controls nicely so they look like Figure 14-16.
Figure 14-16. Controls for the background activity sample
Add the following Imports statement at the top of the source-code file for Form1:
Now add the following source code to the Form1 class:
Private Sub BackgroundActivity_DoWork( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundActivity.DoWork ' ----- The background work starts here. Dim theBackground As BackgroundWorker ' ----- Call the background thread. theBackground = CType(sender, BackgroundWorker) TheBusyWork(theBackground) ' ----- Check for a cancellation. If (theBackground.CancellationPending = True) Then _ e.Cancel = True End Sub Private Sub BackgroundActivity_ProgressChanged( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.ProgressChangedEventArgs) _ Handles BackgroundActivity.ProgressChanged ' ----- The background task updated its progress. WorkProgress.Value = e.ProgressPercentage End Sub Private Sub BackgroundActivity_RunWorkerCompleted( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.RunWorkerCompletedEventArgs) _ Handles BackgroundActivity.RunWorkerCompleted ' ----- Finished. If (e.Cancelled = True) Then WorkStatus.Text = "Cancelled." Else WorkStatus.Text = "Complete." End If WorkProgress.Visible = False WorkProgress.Value = 0 StopWork.Enabled = False StartWork.Enabled = True End Sub Private Sub TheBusyWork(ByVal workerLink As BackgroundWorker) ' ----- Perform some work. For counter As Integer = 1 To 10 ' ----- See if we should jump out now. If (workerLink.CancellationPending = True) Then _ Exit For ' ----- Take a nap for 2 seconds. Threading.Thread.Sleep(2000) ' ----- Inform the primary thread that we've ' made significant progress. workerLink.ReportProgress(counter * 10) Next counter End Sub Private Sub StartWork_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles StartWork.Click ' ----- Start the background process. StartWork.Enabled = False StopWork.Enabled = True WorkStatus.Text = "Progress…" WorkProgress.Value = 0 WorkProgress.Visible = True BackgroundActivity. RunWorkerAsync( ) End Sub Private Sub StopWork_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles StopWork.Click ' ----- Tell the worker thread to stop. BackgroundActivity.CancelAsync( ) End Sub
Run the program, and click on the Start button. The progress will update as the background worker proceeds through its activity loop. You can interrupt the back-ground worker by clicking on the Stop button, although it won't actually stop until the end of the current two-second sleep.
Processes running in Windows have the option of dividing their work among separate threads of execution within those processes. By default, Visual Basic processes include only a single thread: the process itself. However, you can start one or more background worker threads to perform some activity apart from the flow of the primary application.
The .NET Framework includes threading support through the System.Threading namespace, and specifically through the Thread class in that namespace. While using the THRead class is relatively simple, you have to develop or enhance the class if you want standardized interactions to occur between your primary and worker threads.
The BackgroundWorker control, part of the System.ComponentModel namespace, implements a lot of these interaction features for you. To use the control, simply add it to your form. You can also use it as a class by declaring it using the WithEvents keyword:
Private WithEvents BackgroundActivity _ As System.ComponentModel.BackgroundWorker
When you are ready to initiate the background work, call the BackgroundWorker's RunWorkerAsync() method. This triggers the DoWork event. In this event handler, call the method that will perform the background work. The sample code passes the BackgroundWorker instance to the worker method. You don't have to pass this information, but it makes it easier to communicate back to the primary thread if you do.
For example, if you want the worker thread to report its progress, set the control's WorkerReportsProgress property to true, then monitor the control's ProgressChanged event. Calls to the control's ReportProgress() method by the work trigger this event in the primary thread.
This communication works both ways. Setting the control's WorkerSupportsCancellation property to TRue allows the primary thread to request a cancellation of the work by calling the CancelAsync() method. This sets the control's CancellationPending property, as viewed by the worker thread.
Threads make background processing easy, but interactions between threads can be problematic. The issue is that if two threads wish to update the same object instance, there is no guarantee that they will update them in a specific order. Consider a class with three members. Updating these three members occurs over multiple statements:
Private SomeInstance As SomeClass Private Sub UpdateInstance(ByVal scalar As Integer) SomeInstance.Member1 = 10 * scalar SomeInstance.Member2 = 20 * scalar SomeInstance.Member3 = 30 * scalar End Sub
But what happens when two different threads call the UpdateInstance() method at the same time (assuming that they are sharing the SomeInstance variable)? Because of the way that threading works, it's possible that the calls could get interleaved in ways that corrupt the data. Suppose thread #1 calls UpdateInstance(2) and thread #2 calls UpdateInstance(3). It's possible the statements within UpdateInstance() could be called in this order:
SomeInstance.Member1 = 10 * 2 ' From Thread #1 SomeInstance.Member1 = 10 * 3 ' From Thread #2 SomeInstance.Member2 = 20 * 3 ' From Thread #2 SomeInstance.Member2 = 20 * 2 ' From Thread #1 SomeInstance.Member3 = 30 * 2 ' From Thread #1 SomeInstance.Member3 = 30 * 3 ' From Thread #2
After this code, Member1 and Member3 is set based on the call from thread #2, but Member2 retains the value from thread #1.
To prevent this from happening, Visual Basic includes a SyncLock statement that acts as a gatekeeper around a block of code. (The .NET Framework also includes other classes and features that perform a similar service.) Using SyncLock to fix the UpdateInstance() problem, you must create a common object and use it as a locking mechanism:
Private SomeInstance As SomeClass Private LockObject As New Object Private Sub UpdateInstance(ByVal scalar As Integer) SyncLock LockObject SomeInstance.Member1 = 10 * scalar SomeInstance.Member2 = 20 * scalar SomeInstance.Member3 = 30 * scalar End SyncLock End Sub
As each thread enters UpdateInstance(), SyncLock TRies to exclusively lock the LockObject instance. Only when this is successful does the thread proceed through the block of code.