What if you could allow the secondary thread to simply influence objects used by the first thread? You can. For instance, you can build a user interface that spins up a secondary thread that retrieves a DataSet from SQL Server or a Web service and then populates a DataGrid object on a form. However, this introduces the issue of thread safety
since the DataGrid and the form really belong
to the first thread. Let's investigate the issue of thread safety before we proceed.
The great advantage of a free-threaded model is that there can be multiple paths of program execution that run in parallel. All paths share the same resources. Among those resources are things such as variables, fields, properties, and objects. (In an Apartment Threading Model these resources are not shared. See sidebar
for more information.) A problem arises when two or more threads, by sheer coincidence, try to access the same resources.
Let's consider a very simple example. Assume we have an object that stores address information in a number of fields. Let's also assume the object has methods to set and get that information. See Listing 1
and Listing 2
for examples in C# and VB.NET.
While these classes would work perfectly fine in single-threaded environments and they would even work fine most of the time in multi-threaded environments, they are not thread-safe. Let's assume that two different threads call the GetAddress()
methods at roughly the same time. Let's say the thread that called SetAddress()
was slightly ahead of the GetAddress()
method. So SetAddress()
works through the first few lines of code and replaces the two address lines and the city information. Then the other thread kicks in and executed GetAddress()
. Of course at this point in time the information stored in the object is bogus since only half the address has been updated by the first thread.
Scenarios like these lead to bugs that are very hard to find because they occur only sometimes, depending on plain coincidence. This very simple example would result in bogus output as the worst-case scenario. Many thread-safety issues end up more severe with application crashes or worse.
How can you fix the problem? In the current example the main problem is caused by the SetAddress()
method. You would not be bothered at all by multiple threads calling the GetAddress()
method at the same time. While the information is updated you really don't want another thread to mess with your object. You can easily indicate this to the operating system using a so-called sync lock
(in other environments this is known as a critical section
). Both C# and VB.NET consider this issue important enough to make sync locks part of the core language. Here's how you can implement sync locks. C# first:
public bool SetAddress(string addressLine1,
string addressLine2, string city,
string zip, string state, string country)
this.AddressLine1 = addressLine1;
this.AddressLine2 = addressLine2;
this.City = city;
this.ZIP = zip;
this.State = state;
this.Country = country;
And the Visual Basic .NET version:
Public Function SetAddress( _
ByVal addressLine1 As String, _
ByVal addressLine2 As String, _
ByVal city As String, ByVal zip As String, _
ByVal state As String, ByVal country As String)
Me.AddressLine1 = addressLine1
Me.AddressLine2 = addressLine2
Me.City = city
Me.ZIP = zip
Me.State = state
Me.Country = country
Both versions behave identically. Using a sync lock block you indicate to the operating system that a process requires exclusive access to a certain resource (the current objectthis or Me
in this case). All other threads that are trying to gain access to the resource are put into a wait state until the lock ends.
This mechanism solves our problem. However, there are other (different) synchronization issues. Beyond that, sync lock blocks introduce some additional issues, most noteworthy is the issue of deadlocks. Imagine that there are two different types of addresses you need to keep track of (such as a mailing and a billing address). Imagine that one thread locks the mailing address, and by sheer coincidence, a second thread locks the billing address at the same time. The first thread may then try to also lock the billing address. However, it can't do so, until the second thread unlocks that resource. This is where things get critical. If the second thread in fact unlocks the billing address, everything will work out fine. However, if the second thread attempts to lock the mailing address, all hell breaks loose! It simply will never be able to do so, because that address is already locked by the first thread, and will not be unlocked until the first thread is done. The first thread, on the other hand, can't complete before it can lock the second address. That isn't going to happen until the second thread lets go of it, which would only be the case if the second thread could lock the other resource, which it can't since that address is locked by the first thread.
At this point you have two hung threads. There is no technical solution to this logistical problem. The developer has to be very careful with these kinds of nested locks. This type of problem is very hard to debug because it will only occur randomly. The consequence, however, is rather severe.
At this point you might wonder what other problems may occur. While a long list of these types of problems is beyond the scope of this article, I would like to point out one other issue that isn't immediately obvious. Here's another simple VB.NET example:
Public Class Racer
Private Counter As Integer = 0
Public Sub Execute()
Me.Counter = 0
Do While Me.Counter < 1000
Me.Counter = Me.Counter + 1
I designed this simple class to increase a field (Me.Counter
) 1000 times by increments of 1. However, if two threads execute this method at the same time, each thread will attempt to increase the counter, but both threads will only loop 500 times (on average... results will vary) since they try to count up the same member at the same time, (an issue known as racing
The solution to this problem is more of a design issue than it is an implementation issue. You could wrap the loop in a sync lock, but that would defeat the purpose of multi-threading, as the second thread would have to wait for the first one to complete, hence making the process sequential.
Our scenario is not just a logical problem, but a very technical problem as well. Once you've compiled the code a single line such as the Me.Counter = Me.Counter +1
will end up as multiple processing instructions. This means that a thread may be interrupted half way though the execution of a single line of code. Therefore, incrementing a potentially shared resource in the way described above is inherently non-thread-safe.
The same statement would not be true for local variables, which by definition are thread-safe. The following example won't result in any threading issues:
Dim iCounter As Integer
iCounter = 0
Do While iCounter < 1000
iCounter = iCounter + 1
But back to our problematic example! How could you increment the counter field in a thread-safe fashion? The easiest way to do so is through the Interlocked
Similarly, you could decrement a variable using the Decrement()
method or even swap two variables through the Exchange()
method. The field or variable you want to increment has to be passed to the Increment()
method by reference, therefore the C# version would look as follows:
As mentioned above, you may encounter a number of other issues when you're trying to build multi-threaded applications. It is impossible to explore them all within this article. Please refer to the Visual Studio .NET Framework documentation for more information on the subject. There are also a number of books dedicated to the subject of threading.