At this point you should have a basic understanding of threads and how they relate to the process and AppDomain concepts. You should also realize that for interactive applications, multithreading is not a way to improve performance, but rather a way to improve the end user experience by providing the illusion that the computer is executing more code simultaneously. In the case of server-side code, multi-threading enables higher scalability by enabling Windows to better utilize the CPU along with other subsystems such as I/O.
When a background thread is created, it points to a method or procedure that will be executed by the thread. Remember that a thread is just a pointer to the current instruction in a sequence of instructions to be executed. In all cases, the first instruction in this sequence is the start of a method or procedure.
When using the BackgroundWorker control, this method is always the control’s DoWork event handler. Keep in mind that this method can’t be a function. There is no mechanism by which a method running on one thread can return a result directly to code running on another thread. This means that anytime you design a background task, you should start by creating a Sub in which you write the code to run on the background thread.
In addition, because the goals for interactive applications and server programs are different, your designs for implementing threading in these two environments are different. This means that the way you design and code the background task will vary. By way of explanation, let’s work with a simple method that calculates prime numbers. This implementation is naïve, and can take quite a lot of time when run against larger numbers, so it makes for a useful example of a long-running background task. Do the following:
Create a new Windows Forms Application project named Threading.
Add two Button controls, a ListBox and a ProgressBar control to Form1.
Add a BackgroundWorker control to Form1.
Set its WorkerReportsProgress and WorkerSupportsCancellation properties to True.
Add the following to the form’s code:
Public Class Form1 #Region " Shared data " Private mMin As Integer Private mMax As Integer Private mResults As New List(Of Integer) #End Region #Region " Primary thread methods " Private Sub btnStart_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnStart.Click ProgressBar1.Value = 0 ListBox1.Items.Clear() mMin = 1 mMax = 10000 BackgroundWorker1.RunWorkerAsync() End Sub Private Sub btnCancel_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCancel.Click BackgroundWorker1.CancelAsync() End Sub Private Sub BackgroundWorker1_ProgressChanged( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.ProgressChangedEventArgs) _ Handles BackgroundWorker1.ProgressChanged ProgressBar1.Value = e.ProgressPercentage End Sub Private Sub BackgroundWorker1_RunWorkerCompleted( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.RunWorkerCompletedEventArgs) _ Handles BackgroundWorker1.RunWorkerCompleted For Each item As String In mResults ListBox1.Items.Add(item) Next End Sub #End Region #Region " Background thread methods " Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundWorker1.DoWork mResults.Clear() For count As Integer = mMin To mMax Step 2 Dim isPrime As Boolean = True For x As Integer = 1 To CInt(count / 2) For y As Integer = 1 To x If x * y = count Then ' the number is not prime isPrime = False Exit For End If Next ' short-circuit the check If Not isPrime Then Exit For Next If isPrime Then mResults.Add(count) End If Me.BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100)) If Me.BackgroundWorker1.CancellationPending Then Exit Sub End If Next End Sub #End Region End Class
The BackgroundWorker1_DoWork() method implements the code to find the prime numbers. This method is automatically run on a background thread by the BackgroundWorker1 control. Notice that the method is a Sub, so it returns no value. Instead, it stores its results in a variable - in this case, a List(Of Integer). The idea is that once the background task is complete, you can do something useful with the results.
When btnStart is clicked, the BackgroundWorker control is told to start the background task. In order to initialize any data values before launching the background thread, the mMin and mMax variables are set before the task is started.
Of course, you want to display the results of the background task. Fortunately, the BackgroundWorker control raises an event when the task is complete. In this event handler you can safely copy the values from the List(Of Integer) into the ListBox for display to the user.
Similarly, the BackgroundWorker control raises an event to indicate progress as the task runs. Notice that the DoWork() method periodically calls the ReportProgress() method. When this method is called, the progress is transferred from the background thread to the primary thread via the ProgressChanged event.
Finally, you may have the need to cancel a long-running task. It is never wise to directly terminate a background task. Instead, you should send a request to the background task asking it to stop running. This enables the task to cleanly stop running so it can close any resources it might be using and shut down properly.
To send the cancel request, call the BackgroundWorker control’s CancelAsync() method. This sets the control’s CancellationPending property to True. Notice how this value is periodically checked by the DoWork() method; and if it is True, you exit the method, effectively canceling the task.
Running the code now demonstrates that the UI remains entirely responsive while the background task is running, and the results are displayed when available.
Now that we’ve explored the basics of threading in an interactive application, let’s look at the various threading options at our disposal. The .NET Framework offers two ways to implement multithreading. Regardless of which approach you use, you must specify the method or procedure that the thread will execute when it starts.
First, you can use the thread pool provided by the .NET Framework. The thread pool is a managed pool of threads that can be reused over the life of your application. Threads are created in the pool on an as-needed basis, and idle threads in the pool are reused, thus keeping the number of threads created by your application to a minimum. This is important because threads are an expensive operating system resource.
Important | The thread pool should be your first choice in most multithreading scenarios. |
Many built-in .NET Framework features already use the thread pool. In fact, you’ve already used it, because the BackgroundWorker control runs its background tasks on a thread from the thread pool. In addition, anytime you do an asynchronous read from a file, URL, or TCP socket, the thread pool is used on your behalf; and anytime you implement a remoting listener, a website, or a Web Service, the thread pool is used. Because the .NET Framework itself relies on the thread pool, it is an optimal choice for most multithreading requirements.
Second, you can create your own thread object. This can be a good approach if you have a single, long-running background task in your application. It is also useful if you need fine-grained control over the background thread. Examples of such control include setting the thread priority or suspending and resuming the thread’s execution.
The .NET Framework provides a thread pool in the System.Threading namespace. This thread pool is self-managing. It creates threads on demand and, if possible, reuses idle threads that already exist in the pool.
The thread pool won’t create an unlimited number of threads. In fact, it creates at most 25 threads per CPU in the system. If you assign more work requests to the pool than it can handle with these threads, your work requests are queued until a thread becomes available. This is typically a good feature, as it helps ensure that your application won’t overload the operating system with too many threads.
There are five primary ways to use the thread pool: through the BackgroundWorker control, by calling BeginXYZ() methods, via Delegates, manually via the ThreadPool.QueueUserWorkItem() method, or by using a System.Timers.Timer control. Of the five, the easiest is to use the BackgroundWorker control.
The previous quick tour of threading explored the BackgroundWorker control, which enables you to easily start a task on a background thread, monitor that task’s progress, and be notified when it is complete. It also enables you to request that the background task cancel itself. All this is done in a safe manner, with control transferred from the primary thread to the background thread and back again without you having to worry about the details.
Many of the .NET Framework objects support both synchronous and asynchronous invocation. For instance, you can read from a TCP socket by using the Read() method or the BeginRead() method. The Read() method is synchronous, so you’re blocked until the data is read.
The BeginRead() method is asynchronous, so you are not blocked. Instead, the read operation occurs on a background thread in the thread pool. You provide the address of a method that is called automatically when the read operation is complete. This callback method is invoked by the background thread, so your code also ends up running on the background thread in the thread pool.
Behind the scenes, this behavior is all driven by delegates. Rather than explore TCP sockets or some other specific subset of the .NET Framework class library, let’s move on and discuss the underlying technology itself.
A delegate is a strongly typed pointer to a function or method. Delegates are the underlying technology used to implement events within Visual Basic, and they can be used directly to invoke a method, given just a pointer to that method.
Delegates can be used to launch a background task on a thread in the thread pool. They can also transfer a method call from a background thread to the UI thread. The BackgroundWorker control uses this technology behind the scenes on your behalf, but you can use delegates directly as well.
To use delegates, your worker code must be in a method, and you must define a delegate for that method. The delegate is a pointer for the method, so it must have the same method signature as the method itself:
Private Delegate Sub TaskDelegate(ByVal min As Integer, ByVal max As Integer) Private Sub FindPrimesViaDelegate(ByVal min As Integer, ByVal max As Integer) mResults.Clear() For count As Integer = min To max Step 2 Dim isPrime As Boolean = True For x As Integer = 1 To CInt(count / 2) For y As Integer = 1 To x If x * y = count Then ' the number is not prime isPrime = False Exit For End If Next ' short-circuit the check If Not isPrime Then Exit For Next If isPrime Then mResults.Add(count) End If Next End Sub
Running background tasks via delegates enables you to pass strongly typed parameters to the background task, thus clarifying and simplifying your code. Now that you have a worker method and corresponding delegate, you can add a new button and write code in its click event handler to use it to run FindPrimes on a background thread:
Private Sub btnDelegate_Click(ByVal sender As System.Object) ByVal e As System.EventArgs) Handles btnDelegate.Click ' run the task Dim worker As New TaskDelegate(AddressOf FindPrimesViaDelegate) worker.BeginInvoke(1, 10000, AddressOf TaskComplete, Nothing) End Sub
First, you create an instance of the delegate, setting it up to point to the FindPrimesViaDelegate() method. Next, you call BeginInvoke() on the delegate to invoke the method.
The BeginInvoke() method is the key here. BeginInvoke() is an example of the BeginXYZ() methods discussed earlier; and as you’ll recall, they automatically run the method on a background thread in the thread pool. This is true for BeginInvoke() as well, meaning that FindPrimes are run in the background and the UI thread is not blocked, so it can continue to interact with the user.
Notice all the parameters passed to BeginInvoke. The first two correspond to the parameters defined on the delegate - the min and max values that should be passed to FindPrimes. The next parameter is the address of a method that is automatically invoked when the background thread is complete. The final parameter (to which you’ve passed Nothing) is a mechanism by which you can pass a value from your UI thread to the method that is invoked when the background task is complete.
This means that you need to implement the TaskComplete() method. This method is invoked when the background task is complete. It runs on the background thread, not on the UI thread, so remember that this method can’t interact with any Windows Forms objects. Instead, it will contain the code to invoke an UpdateDisplay() method on the UI thread via the form’s BeginInvoke() method:
Private Sub TaskComplete(ByVal ar As IAsyncResult) Dim update As New UpdateDisplayDelegate(AddressOf UpdateDisplay) Me.BeginInvoke(update) End Sub Private Delegate Sub UpdateDisplayDelegate() Private Sub UpdateDisplay() For Each item As String In mResults ListBox1.Items.Add(item) Next End Sub
Notice how a delegate is used to invoke the UpdateDisplay() method as well, thus illustrating how delegates can be used with a Form object’s BeginInvoke() method to transfer control back to the primary thread. The same technique could be used to allow the background task to notify the primary thread of progress as the task runs.
Now when you run the application, you’ll have a responsive UI, with the FindPrimesViaDelegate() method running in the background within the thread pool.
The final option for using the thread pool is to manually queue items for the thread pool to process. This is done by calling ThreadPool.QueueUserWorkItem(). This is a Shared method on the ThreadPool class that directly places a method into the thread pool to be executed on a background thread.
This technique doesn’t allow you to pass arbitrary parameters to the worker method. Instead, it requires that the worker method accept a single parameter of type Object, through which you can pass an arbitrary value. You can use this to pass multiple values by declaring a class with all your parameter types. Add the following class inside the Form1 class:
Private Class params Public min As Integer Public max As Integer Public Sub New(ByVal min As Integer, ByVal max As Integer) Me.min = min Me.max = max End Sub End Class
Then you can make FindPrimes accept this value as an Object:
Private Sub FindPrimesInPool(ByVal state As Object) Dim params As params = DirectCast(state, params) mResults.Clear() For count As Integer = params.min To params.max Step 2 Dim isPrime As Boolean = True For x As Integer = 1 To CInt(count / 2) For y As Integer = 1 To x If x * y = count Then ' the number is not prime isPrime = False Exit For End If Next ' short-circuit the check If Not isPrime Then Exit For Next If isPrime Then mResults.Add(count) End If Next Dim update As New UpdateDisplayDelegate(AddressOf UpdateDisplay) Me.BeginInvoke(update) End Sub
This is basically the same method used with delegates, but it accepts an object parameter, rather than the strongly typed parameters. Notice that the method uses a delegate to invoke the UpdateDisplay method on the UI thread when the task is complete. When you manually put a task on the thread pool, there is no automatic callback to a method when the task is complete, so you must do the callback in the worker method itself.
Now you can manually queue the worker method to run in the thread pool within the Click event handler:
Private Sub btnPool_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnPool.Click ' run the task System.Threading.ThreadPool.QueueUserWorkItem( _ AddressOf FindPrimesInPool, New params(1, 10000)) End Sub
The QueueUserWorkItem() method accepts the address of the worker method - in this case, FindPrimes. This worker method must accept a single parameter of type Object or you’ll get a compile error here.
The second parameter to QueueUserWorkItem() is the object to be passed to the worker method when it is invoked on the background thread. In this case, you’re passing a new instance of the params class defined earlier. This enables you to pass your parameter values to FindPrimes.
When you run this code, you’ll again find that you have a responsive UI, with FindPrimes running on a background thread in the thread pool.
Beyond BeginXYZ() methods, delegates, and manually queuing work items, there are various other ways to get your code running in the thread pool. One of the most common is through the use of a special Timer control. The Elapsed event of this control is raised on a background thread in the thread pool.
This is different from the System.Windows.Forms.Timer control, where the Tick event is raised on the UI thread. The difference is very important to understand, because you can’t directly interact with Windows Forms objects from background threads. Code running in the Elapsed event of a System .Timers.Timer control must be treated like any other code running on a background thread.
The exception to this is if you set the SynchronizingObject property on the control to a Windows Forms object such as a Form or a Control. In this case, the Elapsed event is raised on the appropriate UI thread, rather than on a thread in the thread pool. The result is basically the same as using System.Windows.Forms.Timer instead.
Thus far, we’ve been working with the .NET thread pool. You can also manually create and control background threads through code. To manually create a thread, you need to create and start a Thread object. This looks something like the following:
' run the task Dim worker As New Thread(AddressOf FindPrimes) worker.Start()
While this seems like the obvious way to do multithreading, the thread pool is typically the preferred approach. This is because there is a cost to creating and destroying threads, and the thread pool helps avoid that cost by reusing threads when possible. When you manually create a thread as shown here, you must pay the cost of creating the thread each time or implement your own scheme to reuse the threads you create.
However, manual creation of threads can be useful. The thread pool is designed to be used for background tasks that run for a while and then complete, thus allowing the background thread to be reused for subsequent background tasks. If you need to run a background task for the entire duration of your application, the thread pool is not ideal, because that thread would never become available for reuse. In such a case, you’re better off creating the background thread manually.
An example of this is the aforementioned spell checker in Word, which runs as long as you’re editing a document. Running such a task on the thread pool would make little sense, as the task will run as long as the application, so instead it should be run on a manually created thread, leaving the thread pool available for shorter running tasks.
The other primary reason for manual creation of threads is when you want to be able to interact with the Thread object as it is running. You can use various methods on the Thread object to interact with and control the background thread. These are described in the following table:
Method | Description |
---|---|
Abort | Stops the thread. This is not recommended, as no cleanup occurs. This is not a graceful shutdown of the thread. |
ApartmentState | Sets the COM apartment type used by this thread - important if you’re using COM interop in the background task |
Join | Blocks your current thread until the background thread is complete |
Priority | Enables you to raise or lower the priority of the background thread so Windows will schedule it to get more or less CPU time relative to other threads |
Sleep | Causes the thread to be suspended for a specified period of time |
Suspend | Suspends a thread - temporarily stopping it without terminating the thread |
Resume | Restarts a suspended thread |
Many other methods are available on the Thread object as well; consult the online help for more details. You can use these methods to control the behavior and lifetime of the background thread, which can be useful in advanced threading scenarios.
In most multithreading scenarios, you have data in your main thread that needs to be used by the background task on the background thread. Likewise, the background task typically generates data that is needed by the main thread. These are examples of shared data, or data that is used by multiple threads.
Remember that multithreading means you have multiple threads within the same process, and in .NET within the same AppDomain. Because memory within an AppDomain is common across all threads in that AppDomain, it is very easy for multiple threads to access the same objects or variables within your application.
For example, in our original prime example, the background task needed the min and max values from the main thread, and all the implementations have used a List(Of Integer) to transfer results back to the main thread when the task was complete. These are examples of shared data. Note that we didn’t do anything special to make the data shared - the variables were shared by default.
When you’re writing multithreaded code, the trickiest issue is managing access to shared data within your AppDomain. You don’t, for example, want two threads writing to the same piece of memory at the same time. Equally, you don’t want a group of threads reading memory that another thread is in the process of changing. This management of memory access is called synchronization. It’s properly managing synchronization that makes writing multithreaded code difficult.
When multiple threads want to simultaneously access a common bit of shared data, use synchronization to control things. This is typically done by blocking all but one thread, so only one thread can access the shared data. All other threads are put into a wait state by using a blocking operation of some sort. Once the nonblocked thread is done using the shared data, it releases the block, allowing another thread to resume processing and to use the shared data.
The process of releasing the block is often called an event. When we say “event,” we are not talking about a Visual Basic event. Although the naming convention is unfortunate, the principle is the same - something happens and we react to it. In this case, the nonblocked thread causes an event, which releases some other thread so it can access the shared data.
Although blocking can be used to control the execution of threads, it’s primarily used to control access to resources, including memory. This is the basic idea behind synchronization - if you need something, you block until you can access it.
Synchronization is expensive and can be complex. It is expensive because it stops one or more threads from running while another thread uses the shared data. The whole point of having multiple threads is to do more than one thing at a time, and if you’re constantly blocking all but one thread, then you lose this benefit.
It can be complex because there are many ways to implement synchronization. Each technique is appropriate for a certain class of synchronization problem, and using the wrong one in the wrong place increases the cost of synchronization.
It is also quite possible to create deadlocks, whereby two or more threads end up permanently blocked. You’ve undoubtedly seen examples of this. Pretty much anytime a Windows application totally locks up and must be stopped by the Task Manager, you are seeing an example of poor multithreading implementation. The fact that this happens even in otherwise high-quality commercial applications (such as Microsoft Outlook) is confirmation that synchronization can be very hard to get right.
Because synchronization has so many downsides in terms of performance and complexity, the best thing you can do is avoid or minimize its use. If at all possible, design your multithreaded applications to avoid reliance on shared data, and to maintain tight control over the use of any shared data that is required.
Typically, some shared data is unavoidable, so the question becomes how to manage that shared data to avoid or minimize synchronization. Two primary schemes are used for this purpose.
The first approach is to avoid sharing of data by always passing references to the data between threads. If you also ensure that neither thread uses the same reference, then each thread has its own copy of the data, and no thread needs access to data being used by any other threads.
This is exactly what you did in the prime example where you started the background task via a delegate:
Dim worker As New TaskDelegate(AddressOf FindPrimesViaDelegate) worker.BeginInvoke(1, 10000, AddressOf TaskComplete, Nothing)
The min and max values are passed as ByVal parameters, meaning they are copied and provided to the indPrimes() method. No synchronization is required here because the background thread never tries to access the values from the main thread. We passed copies of the values a different way when we manually started the task in the thread pool:
System.Threading.ThreadPool.QueueUserWorkItem( _ AddressOf FindPrimesInPool, New params(1, 10000))
In this case, we created a params object into which we put the min and max values. Again, those values were copied before they were used by the background thread. The FindPrimesInPool() method never attempted to access any parameter data being used by the main thread.
What we’ve done so far works great for variables that are value types, such as Integer, and immutable objects, such as String. It won’t work for reference types, such as a regular object, because reference types are never passed by value, only by reference.
To use reference types, we need to change our approach. Rather than return a copy of the data, we’ll return a reference to the object containing the data. Then we’ll make sure that the background task stops using that object, and starts using a new object. As long as different threads aren’t simultaneously using the same objects, there’s no conflict.
You can enhance the prime application to provide the prime numbers to the UI thread as it finds them, rather than in a batch at the end of the process. To see how this works, we’ll alter the original code based on the BackgroundWorker control. That is the easiest, and typically the best, way to start a background task, so we’ll use it as a base implementation.
The first thing to do is to alter the DoWork() method so it periodically returns results. Rather than use the shared mResults variable, we’ll use a local List(Of Integer) variable to store the results. Each time we have enough results to report, we’ll return that List(Of Integer) to the UI thread, and create a new List(Of Integer) for the next batch of values. This way, we’re never sharing the same object between two threads. The required changes are highlighted:
Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundWorker1.DoWork 'mResults.Clear() Dim results As New List(Of Integer) For count As Integer = mMin To mMax Step 2 Dim isPrime As Boolean = True For x As Integer = 1 To CInt(count / 2) For y As Integer = 1 To x If x * y = count Then ' the number is not prime isPrime = False Exit For End If Next ' short-circuit the check If Not isPrime Then Exit For Next If isPrime Then 'mResults.Add(count) results.Add(count) If results.Count >= 10 Then BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) results = New List(Of Integer) End If End If BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100)) If BackgroundWorker1.CancellationPending Then Exit Sub End If Next BackgroundWorker1.ReportProgress(100, results) End Sub
The results are now placed into a local List(Of Integer). Anytime the list has 10 values, we return it to the primary thread by calling the BackgroundWorker control’s ReportProgress() method, passing the List(Of Integer) as a parameter.
The important thing here is to then immediately create a new List(Of Integer) for use in the DoWorker() method. This ensures that the background thread is never trying to interact with the same List(Of Integer) object as the UI thread.
Now that the DoWork() method is returning results, alter the code on the primary thread to use those results:
Private Sub BackgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles BackgroundWorker1.ProgressChanged ProgressBar1.Value = e.ProgressPercentage If e.UserState IsNot Nothing Then For Each item As String In CType(e.UserState, List(Of Integer)) ListBox1.Items.Add(item) Next End If End Sub
Anytime the ProgressChanged event is raised, the code checks to see whether the background task provided a state object. If it did provide a state object, then you cast it to a List(Of Integer) and update the UI to display the values in the object.
At this point, you no longer need the RunWorkerCompleted() method, so it can be removed or commented out. If you run the code at this point, not only is the UI continually responsive, but the results from the background task are displayed as they are discovered, rather than in a batch at the end of the process. As you run the application, resize and move the form while the prime numbers are being found. Although the displaying of the data may be slowed down as you interact with the form (because the UI thread can only do so much work), the generation of the data continues independently in the background and is not blocked by the UI thread’s work.
When you rely on transferring data ownership, you ensure that only one thread can access the data at any given time by ensuring that the background task never uses an object once it returns it to the primary thread.
So far, you’ve seen ways to avoid the sharing of data, but sometimes you’ll have a requirement for data sharing, in which case you’ll be faced with the complex world of synchronization.
As discussed earlier, incorrect implementation of synchronization can cause performance issues, dead-locks, and application crashes. Success is dependent on serious attention to detail. Problems may not manifest in testing, but when they happen in production they are often catastrophic. You can’t test to ensure proper implementation; you must prove it in the same way mathematicians prove mathematical truths - by careful logical analysis of all possibilities.
Some objects in the .NET Framework have built-in support for synchronization, so you don’t need to write it yourself. In particular, most of the collection-oriented classes have optional support for synchronization, including Queue, Stack, Hashtable, ArrayList, and more.
Rather than transfer ownership of List(Of Integer) objects from the background thread to the UI thread as shown in the last example, you can use the synchronization provided by the ArrayList object to help mediate between the two threads.
To use a synchronized ArrayList, you need to change from the List(Of Integer) to an ArrayList. Additionally, the ArrayList must be created a special way:
Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundWorker1.DoWork 'mResults.Clear() 'Dim results As New List(Of Integer) Dim results As ArrayList = ArrayList.Synchronized(New ArrayList)
What you’re doing here is creating a normal ArrayList, and then having the ArrayList class “wrap” it with a synchronized wrapper. The result is a thread-safe ArrayList object that automatically prevents multiple threads from interacting with the data in invalid ways.
Now that the ArrayList is synchronized, you don’t need to create a new one each time you return the values to the primary thread. Comment out the following line in the DoWork() method:
If results.Count >= 10 Then BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) 'results = New List(Of Integer) End If
Finally, update the code on the primary thread to properly display the data from the ArrayList: Private Sub BackgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles BackgroundWorker1.ProgressChanged ProgressBar1.Value = e.ProgressPercentage If e.UserState IsNot Nothing Then Dim result As ArrayList = CType(e.UserState, ArrayList) For index As Integer = ListBox1.Items.Count To result.Count - 1 ListBox1.Items.Add(result(index)) Next End If End Sub
Because the entire list is accessible at all times, you need only copy the new values to the ListBox, rather than loop through the entire list. This works out well anyway, because the For Each statement isn’t thread safe even with a synchronized collection. To use the For Each statement, you’d need to enclose the entire loop inside a SyncLock block:
Dim result As ArrayList = CType(e.UserState, ArrayList) SyncLock result.SyncRoot For Each item As String in result ListBox1.Items.Add(item) Next End SyncLock
The SyncLock statement in Visual Basic is used to provide an exclusive lock on an object. Here it is being used to get an exclusive lock on the ArrayList object’s SyncRoot. This means all the code within the SyncLock block can be sure that it is the only code interacting with the contents of the ArrayList. No other threads can access the data while your code is in this block.
While many collection objects optionally provide support for synchronization, most objects in the .NET Framework or in third-party libraries are not thread safe. To safely share these objects and classes in a multithreaded environment, you must manually implement synchronization.
To manually implement synchronization, you must rely on help from the Windows operating system. The .NET Framework includes classes that wrap the underlying Windows operating system concepts, however, so you don’t need to call Windows directly. Instead, you use the .NET Framework synchronization objects.
Synchronization objects have their own special terminology. Most of these objects can be acquired and released. In other cases, you wait on an object until it is signaled.
For objects that can be acquired, the idea is that when you have the object, you have a lock. Any other threads trying to acquire the object are blocked until you release the object. These types of synchronization objects are like a hot potato - only one thread has it at a time and other threads are waiting for it. No thread should hold onto such an object any longer than necessary, as that slows down the whole system.
The other class of objects comprises those that wait on the object - which means your thread is blocked. Some other thread will signal your object, which releases you (to become unblocked). Many threads can be waiting on the same object, and when the object is signaled, all the blocked threads are released. This is basically the exact opposite of an acquire/release type object. The following table lists the primary synchronization objects in the .NET Framework:
Object | Model | Description |
---|---|---|
AutoResetEvent | Wait/Signal | Allows a thread to release other threads that are waiting on the object |
Interlocked | N/A | Allows multiple threads to safely increment and decrement values that are stored in variables accessible to all the threads |
ManualResetEvent | Wait/Signal | Allows a thread to release other threads that are waiting on the object |
Monitor | Acquire/Release | Defines an exclusive application-level lock whereby only one thread can hold the lock at any given time |
Mutex | Acquire/Release | Defines an exclusive systemwide lock whereby only one thread can hold the lock at any given time |
ReaderWriterLock | Acquire/Release | Defines a lock whereby many threads can read data, but exclusive access is provided to one thread for writing data |
Perhaps the easiest type of synchronization to understand and implement is an exclusive lock. When one thread holds an exclusive lock, no other thread can obtain that lock. Any other thread attempting to obtain the lock is blocked until the lock becomes available.
There are two primary technologies for exclusive locking: the monitor and mutex objects. The monitor object allows a thread in a process to block other threads in the same process. The mutex object allows a thread in any process to block threads in the same process or in other processes. Because a mutex has systemwide scope, it is a more expensive object to use and should only be used when cross-process locking is required.
Visual Basic includes the SyncLock statement, which is a shortcut to access a monitor object. While it is possible to directly create and use a System.Threading.Monitor object, it is far simpler to just use the SyncLock statement (briefly mentioned in the ArrayList object discussion), so that is what we’ll do here.
Exclusive locks can be used to protect shared data so only one thread at a time can access the data. They can also be used to ensure that only one thread at a time can run a specific bit of code. This exclusive bit of code is called a critical section. While critical sections are an important concept in computer science, it is far more common to use exclusive locks to protect shared data, and that’s what this chapter focuses on.
You can use an exclusive lock to lock virtually any shared data. For example, you can change your code to use the SyncLock statement instead of a synchronized ArrayList. Change the declaration of the ArrayList in the DoWork method so it is global to the form and no longer synchronized:
Private results As New ArrayList
This means you’re responsible for managing all synchronization yourself. First, in the DoWork method, protect all access to the results variable:
If isPrime Then Dim numberOfResults As Integer SyncLock results.SyncRoot results.Add(count) numberOfResults = results.Count End SyncLock If numberofresults >= 10 Then BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) End If End If
Notice how the code has changed so both the Add() and Count() method calls are contained within a SyncLock block. This ensures that no other thread can be interacting with the ArrayList while you make these calls. The SyncLock statement acts against an object - in this case, results.SyncRoot.
Tip | The trick to making this work is to ensure that all code throughout the application wraps any access to results within the SyncLock statement. If any code doesn’t follow this protocol, then there will be conflicts between threads! |
Because SyncLock acts against a specific object, you can have many active SyncLock statements, each working against a different object:
SyncLock obj1 ' blocks against obj1 End SyncLock SyncLock obj2 ' blocks against obj2 End SyncLock
Note that neither obj1 nor obj2 is altered or affected by this at all. The only thing you’re saying here is that while you’re within a SyncLock obj1 code block, any other thread attempting to execute a SyncLock obj1 statement will be blocked until you’ve executed the End SyncLock statement.
Next, change the UI update code in the ProgressChanged method:
ProgressBar1.Value = e.ProgressPercentage If e.UserState IsNot Nothing Then Dim result As ArrayList = CType(e.UserState, ArrayList) SyncLock result For index As Integer = ListBox1.Items.Count To result.Count - 1 ListBox1.Items.Add(result(index)) Next End SyncLock End If
Again, notice how the interaction with the ArrayList is contained within a SyncLock block. While this version of the code will operate just fine, it is very slow. In fact, you can pretty much stall out the whole processing by continually moving or resizing the window while it runs. This is because the UI thread is blocking the background thread via the SyncLock call, and if the UI thread is totally busy moving or resizing the window, then the background thread can be entirely blocked during that time as well.
While exclusive locks are an easy way to protect shared data, they are not always the most efficient. Your application will often contain some code that is updating shared data, and other code that is only reading from shared data. Some applications do a great deal of data reading, and only periodic data changes.
Because reading data doesn’t change anything, there’s nothing wrong with having multiple threads read data at the same time, as long as you can ensure that no threads are updating data while you’re trying to read. In addition, you typically only want one thread updating at a time.
What we have then is a scenario in which we want to allow many concurrent readers, but if the data is to be changed, then one thread must temporarily gain exclusive access to the shared memory. This is the purpose behind the ReaderWriterLock object.
Using a ReaderWriterLock, you can request either a read lock or a write lock. If you obtain a read lock, you can safely read the data. Other threads can simultaneously obtain read locks and safely read the data.
Before you can update data, you must obtain a write lock. When you request a write lock, any other threads requesting either a read or write lock are blocked. If any outstanding read or write locks are in progress, you’ll be blocked until they are released. Once there are no outstanding locks (read or write), you’ll be granted the write lock. No other locks are granted until you release the write lock, so your write lock is an exclusive lock.
Once you release the write lock, any pending requests for other locks are granted, allowing either another single writer to access the data or multiple readers to simultaneously access the data. You can adapt the sample code to use a System.Threading.ReaderWriterLock object. Start by using the code that was just created based on the SyncLock statement with a Queue object as shared data. First, create an instance of the ReaderWriterLock in a formwide variable:
' lock object Private mRWLock As New System.Threading.ReaderWriterLock
Because a ReaderWriterLock is just an object, you can have many lock objects in an application if needed. You could use each lock object to protect different bits of shared data. Then you can change the DoWork method to make use of this object instead of the SyncLock statement:
If isPrime Then Dim numberOfResults As Integer mRWLock.AcquireWriterLock(100) Try results.Add(count) Finally mRWLock.ReleaseWriterLock() End Try mRWLock.AcquireReaderLock(100) Try numberOfResults = results.Count Finally mRWLock.ReleaseReaderLock() End Try If numberOfResults >= 10 Then BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) End If End If
Before you write or alter the data in the ArrayList, you need to acquire a writer lock. Before reading any data from the ArrayList, you need to acquire a reader lock.
If any thread holds a reader lock, attempts to get a writer lock are blocked. When any thread requests a writer lock, any other requests for a reader lock are blocked until after that thread gets (and releases) its writer lock. In addition, if any thread has a writer lock, other threads requesting a reader (or writer) lock are blocked until that writer lock is released.
The result is that there can be only one writer, and while the writer is active, there are no readers. However, if no writer is active, then there can be many concurrent reader threads running at the same time.
Note that all work done while a lock is held is contained within a Try..Finally block. This ensures that the lock is released regardless of any exceptions you might encounter.
Important | It is critical to always release locks you’re holding. Failure to do so may cause your application to become unstable and crash or lock up unexpectedly. |
Failure to release a lock will almost certainly block other threads, possibly forever - causing a deadlock situation. The alternate fate is that the other threads will request a lock and time out, throwing an exception and causing the application to fail. Either way, if you don’t release your locks, you’ll cause application failure.
Now, update the code in the ProgressChanged method:
ProgressBar1.Value = e.ProgressPercentage If e.UserState IsNot Nothing Then Dim result As ArrayList = CType(e.UserState, ArrayList) mRWLock.AcquireReaderLock(100) Try For index As Integer = ListBox1.Items.Count To result.Count - 1 ListBox1.Items.Add(result(index)) Next Finally mRWLock.ReleaseReaderLock() End Try End If
Again, before reading from results, you get a reader lock, releasing it in a Finally block once you’re done. This code will run a bit smoother than the previous implementation, but the UI thread can be kept busy with resizing or moving the window, thus causing it to hold the reader lock and preventing the background thread from running, as it won’t be able to acquire a writer lock.
Both the Monitor (SyncLock) and ReaderWriterLock objects follow the acquire/release model, whereby threads are blocked until they can acquire control of the appropriate lock.
You can flip the paradigm by using the AutoResetEvent and ManualResetEvent objects. With these objects, threads voluntarily wait on the event object. While waiting, they are blocked and do no work. When another thread signals (raises) the event, any threads waiting on the event object are released and do work.
You can signal an event object by calling the object’s Set() method. To wait on an event object, a thread calls that object’s WaitOne() method. This method blocks the thread until the event object is signaled (the event is raised).
Event objects can be in one of two states: signaled or not signaled. When an event object is signaled, threads waiting on the object are released. If a thread calls WaitOne() on an event object that is signaled, then the thread isn’t blocked and continues running. However, if a thread calls WaitOne() on an event object that is not signaled, then the thread is blocked until some other thread calls that object’s Set() method, thus signaling the event.
AutoResetEvent objects automatically reset themselves to the not signaled state as soon as any thread calls the WaitOne() method. In other words, if an AutoResetEvent is not signaled and a thread calls WaitOne(), then that thread will be blocked. Another thread can then call the Set method, thus signaling the event. This both releases the waiting thread and immediately resets the AutoResetEvent object to its not signaled state.
You can use an AutoResetEvent object to coordinate the use of shared data between threads. Change the ReaderWriterLock declaration to declare an AutoResetEvent instead:
Dim mWait As New System.Threading.AutoResetEvent(False)
By passing False to the constructor, you’re telling the event object to start out in its not signaled state. Were you to pass True, it would start out in the signaled state, and the first thread to call WaitOne would not be blocked, but would trigger the event object to automatically reset its state to not signaled.
Next, you can update DoWork() to use the event object. In order to ensure that both the primary and background threads don’t simultaneously access the ArrayList object, use the AutoResetEvent object to block the background thread until the UI thread is done with the ArrayList:
If isPrime Then Dim numberOfResults As Integer results.Add(count) numberOfResults = results.Count If numberOfResults >= 10 Then BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) mWait.WaitOne() End If End If
This code is much simpler than using the ReaderWriterLock. In this case, the background thread assumes it has exclusive access to the ArrayList until the ReportProgress method is called to invoke the primary thread to update the UI. When that occurs, the background thread calls the WaitOne method so it is blocked until released by the primary thread.
In the UI update code, change the code to release the background thread:
ProgressBar1.Value = e.ProgressPercentage If e.UserState IsNot Nothing Then Dim result As ArrayList = CType(e.UserState, ArrayList) For index As Integer = ListBox1.Items.Count To result.Count - 1 ListBox1.Items.Add(result(index)) Next mWait.Set() End If
This is done by calling the Set() method on the AutoResetEvent object, thus setting it to its signaled state. This releases the background thread so it can continue to work. Notice that the Set method isn’t called until after the primary thread is completely done working with the ArrayList object.
As with the previous examples, if you continually move or resize the form, then the UI thread becomes so busy it won’t ever release the background thread.
A ManualResetEvent object is very similar to the AutoResetEvent just used. The difference is that with a ManualResetEvent object you’re in total control over whether the event object is set to its signaled or not signaled state. The state of the event object is never altered automatically.
This means you can manually call the Reset method, rather than rely on it to occur automatically. The result is that you have more control over the process and can potentially gain some efficiency.
To see how this works, change the declaration to create a ManualResetEvent:
' wait object Dim mWait As New System.Threading.ManualResetEvent(True)
Notice that you’re constructing it with a True parameter. This means that the object will initially be in its signaled state. Until it is reset to a nonsignaled state, WaitOne() calls won’t block on this object.
Change the DoWork() method as follows:
If isPrime Then mWait.WaitOne() Dim numberOfResults As Integer results.Add(count) numberOfResults = results.Count If numberOfResults >= 10 Then mWait.Reset() BackgroundWorker1.ReportProgress( _ CInt((count - mMin) / (mMax - mMin) * 100), results) End If End If
This is quite different from the previous code. Before interacting with the ArrayList object, the code calls WaitOne(). This causes it to block if the primary thread is active. Remember that initially the lock object is signaled, so the WaitOne() call will not block.
Then, before transferring control to the primary thread to update the UI, you call mWait.Reset(). The Reset event sets the lock object to its nonsignaled state. Until its Set() method is called, any WaitOne() methods will block. No changes are required to the UI update code. It already calls the Set() method when it is done interacting with the ArrayList.
The result is that the background thread can continue to search for prime numbers while the UI is being updated. The only time the background thread will block is if it finds a prime number before the UI is done with its update process.