Asynchronous programming provides you with access to lightweight multithreading. The ThreadPool and Thread classes provide you with heavyweight multithreading. The ThreadPool class manages a collection of worker-thread objects waiting for work. Because ThreadPool has threads waiting around, this class may be more efficient to use. Alternatively, you can use the Thread class directly, but while this gives you a little more control, the Thread class also requires more responsibility.
As a general practice there is no reason why you cannot use the ThreadPool class for all your threading needs. The ThreadPool class is a bit easier to use, and you will get the same multithreaded performance results with a bit less work than if you create instances of the Thread class directly.
The remainder of this chapter demonstrates how to use multithreading in Visual Basic .NET using threads from the ThreadPool class as well as the Thread class. Finally, I will show you how to tie it all together to use multithreading in Windows Forms applications.
Multithreading with the ThreadPool Class
You get almost the same benefit from using the ThreadPool class as you do from using the Thread class. Both provide a means of multithreading in VB .NET and both incur almost the same liability. The liability is that threads can be significantly more challenging to debug than single-threaded applications because of very subtle bugs that are hard to find.
I will demonstrate how to use the ThreadPool class in this subsection and the Thread class later in this chapter. Although I encourage you to use threading in VB .NET, I recommend that you use multithreading sparingly and be prepared to expend extra effort in code reviews and testing the threaded parts of your applications.
The ThreadPool class manages the creation and destruction of threads on your behalf ; all you have to do is provide the ThreadPool class with some work to do. The work is indicated using a delegate (again illustrating the importance of delegates in VB .NET). Listing 6.9 demonstrates the mechanics of using the ThreadPool class in multithreading.
Listing 6.9 Basic Multithreading with the ThreadPool Class and Delegates
1: ' Caution: This console application has a bug in it. 2: ' It was written expressly to demonstrate 3: ' the mechanics of the ThreadPool class and problems 4: ' with shared data accessed from multiple threads. 5: 6: Imports System.Threading 7: 8: Module Module1 9: 10: Private Value As Integer = 0 11: Private Reset As ManualResetEvent = New ManualResetEvent(False) 12: 13: Sub Main() 14: ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf Increment)) 15: Increment2() 16: Reset.WaitOne() 17: Console.WriteLine(Value) 18: Console.ReadLine() 19: End Sub 20: 21: Public Sub Increment(ByVal State As Object) 22: Dim I As Integer 23: For I = 1 To 100 24: Value += 1 25: Next 26: 27: Reset.Set() 28: End Sub 29: 30: Public Sub Increment2() 31: Dim I As Integer 32: For I = 1 To 100 33: Value += 1 34: Next 35: End Sub 36: 37: End Module
The example application in Listing 6.9 is a console application defined in ThreadPoolDemo.sln . The Main subroutine in lines 13 through 19 represents the main thread and line 14 represents the second thread created using ThreadPool .
To create a thread using ThreadPool we pass an instance of a WaitCallback delegate to the shared method, ThreadPool.QueueUserWorkItem . A WaitCallback delegate is a subroutine that has a single argument, an Object .
The subroutines Increment and Increment2 run on separate threads and increment the variable Value by 1 up to 100. The value of Value after both threads run should be 200. (And it probably will be every time.)
The code works as follows . Line 14 queues a work item into ThreadPool , which will grab an available thread or create a new one. We don't care; that's the job of the ThreadPool class. Line 15 invokes Increment2 . Because both methods are short, Increment2 will probably finish before Increment because it takes a smidgen of time longer to start up the thread in the queue. However, each method runs on a separate thread, which you can verify by opening the Thread window in Visual Studio .NET.
Line 16 uses ManualResetEvent , which inherits from WaitHandle , to block until Increment signals ManualResetEvent in line 27. Without ManualResetEvent this application would finish before the increment on the second thread would have a chance to finish. When the second thread is finished, Value is written to the console, and the Console.ReadLine method waits for the user to press enter before closing the console window.
To summarize, here are the basic steps for multithreading with the ThreadPool class.
Exploring Thread Safety
What about thread safety? We are accessing Value from two separate threads and Reset from a different thread than the one it was created on. Is this safe? Let's closely examine what is going on here.
First, it is important to realize that the module concept was carried over from Visual Basic 6, but a module really equates to a class in which every member is shared. This means that all members of a console application can be accessed from any thread. Listing 6.10 illustrates the module and class relationship.
Listing 6.10 Illustrating the Relationship between Modules and Classes
1: Imports System.Threading 2: 3: 4: Public Class Class1 5: 6: Private Shared Value As Integer = 0 7: Private Shared Reset As ManualResetEvent = _ 8: New ManualResetEvent(False) 9: Public Shared Sub Main() 10: 11: Dim C As Class1 = New Class1() 12: ThreadPool.QueueUserWorkItem( _ 13: New WaitCallback(AddressOf C.Increment)) 14: C.Increment2() 15: Reset.WaitOne() 16: Console.WriteLine(Value) 17: Console.ReadLine() 18: End Sub 19: 20: Public Sub Increment(ByVal State As Object) 21: Dim I As Integer 22: For I = 1 To 100 23: Value += 1 24: Next 25: Reset.Set() 26: End Sub 27: 28: Public Sub Increment2() 29: Dim I As Integer 30: For I = 1 To 100 31: Value += 1 32: Next 33: End Sub 34: 35: End Class
Notice that Listing 6.10 defines a class, Class1 , with a shared Main subroutine. Class1.Main can be used as a startup routine just as Module1.Main was. For instance, C# does not have the module idiom, and as a result you have to create console applications in C# by using a class with a static (shared) Main method.
Thus, we now know that module members are really shared class members and that we must treat them as such. Back to our question: Is it safe to modify Value and Reset from different threads? The answer is no.
When you run the code in Listing 6.9 (or 6.10) you will get the right result for very large numbers , even in this simple application. Correct results might lead you to believe that the program is working correctly; but it is not.
Lines of code yield several lines of assembly code, and a multithreaded application can interrupt another thread down to the individual lines of assembly code. This means that the intermediate modifications to Value could be trounced by another thread. As defined, the methods Increment and Increment2 execute so quickly you are unlikely to detect this problem. However, if you change the upper limit of each loop from 100 to 10,000,000so that these methods take a few milliseconds to runyou will then sporadically get incorrect results. Our example is a very simple application; imagine how subtle the threaded bugs might be in a production application. For this reason it is important to scrutinize multithreaded code very carefully , especially if it runs critical systems that may adversely impact human life. In a moment, we will talk about making the application thread-safe.
What about the ManualResetEvent object in line 11 of Listing 6.9are we using ManualResetEvent safely there? The answer is yes. Why? We are modifying ManualResetEvent from only a single thread, so there is no chance that a second thread will goof it up for us. If only one thread accesses shared data, there are no worries.
Employing Multithreading Safely
Unfortunately, there is no way to write guidelines for every situation, but we can examine how to make Listing 6.9 (or 6.10) interact with the shared integer correctly. We can also derive some basic guidelines for multithreading in general.
The shared variable Value in Listing 6.9implicit as a member of a moduleand in Listing 6.10 needs to block when it is being modified. We can use the SyncLock statement to make sure that Value is incremented without interruption by another thread (Listing 6.11).
Listing 6.11 Using SyncLock to Enhance Thread Safety
1: Imports System.Threading 2: 3: Module Module1 4: 5: Private Value As Integer = 0 6: Private Reset As ManualResetEvent = New ManualResetEvent(False) 7: 8: Sub Main() 9: ThreadPool.QueueUserWorkItem( _ 10: New WaitCallback(AddressOf Increment)) 11: Increment2() 12: Reset.WaitOne() 13: Console.WriteLine(Value) 14: Console.ReadLine() 15: End Sub 16: 17: Public Sub Increment(ByVal State As Object) 18: Dim I As Integer 19: For I = 1 To 10000000 20: AddOne() 21: Next 22: Reset.Set() 23: End Sub 24: 25: Public Sub Increment2() 26: Dim I As Integer 27: For I = 1 To 10000000 28: AddOne() 29: Next 30: End Sub 31: 32: Public Sub AddOne() 33: SyncLock GetType(Module1) 34: Value += 1 35: End SyncLock 36: End Sub 37: 38: End Module
Listing 6.11 contains code almost identical to Listing 6.9. The only difference is that in this new listing we actually modify Value in a method named AddOne . Wrapped around the statementline 34that modifies the shared (or, effectively, global) variable Value is what is referred to as a mutex . SyncLock accepts an expression that yields a unique value. The Type object for a type is commonly used as demonstrated in line 33. Until the End SyncLock statement (line 35) executes, all other threads will be blocked at line 33.
Using SyncLock works well. However, a problem can occur if what is referred to as a deadlock occurs. A deadlock exists when there are two or more interdependent threads, for example, Thread A is waiting on thread B while B is waiting on A. Deadlock can be difficult to debug. The more complex and diverse the interactions between threads, the more difficult bugs caused by multithreading can be to resolve. The following general practices can help diminish the number of defects caused by multithreading.
Following the guidelines above will help you eliminate common multithreading problems. Be prepared to spend a disproportionately large amount of time testing, reviewing, and debugging the multithreaded parts of your applications, making sure that the payoff for using threads is large enough to make the extra development time worth the effort.
Multithreading with the Thread Class
The same problems that you may encounter with the ThreadPool class exist for the Thread class as well. As a result, the same guidelines apply too. (Refer to the previous subsection, Multithreading with the ThreadPool Class, for a general discussion of threading problems and resolutions .) I encourage you to use the ThreadPool class for the specific reason that it is easier to do so. With that in mind, this section demonstrates how to use the Thread class since you are likely to encounter it in someone else's code and may occasionally elect to use the Thread class directly yourself.
The Thread class works very similarly to the ThreadPool class. Whereas the ThreadPool class contains and manages worker threads, when you create an instance of the Thread class, you have a single thread, and you have to manage every aspect of that thread yourself. Thus, you have more control, but with control comes responsibility.
The basic process for multithreading with the Thread class is outlined below.
Optionally, you can set the Thread.IsBackground property to True . This will cause the background thread to terminate when the main thread terminates. You can set Thread.Priority to ThreadPriority.BelowNormal or ThreadPriority.Lowest to let the graphical user interface have a higher priority. Finally, you can invoke Sleep , Interrupt , Suspend , Join , Resume , or Abort to exert control over individual threads. As is true with threads in the ThreadPool class, use ManualResetEvent or AutoResetEvent as a means of blocking while waiting on a thread. For familiarity , Listing 6.12 reimplements the code in Listing 6.10, using the Thread class.
Listing 6.12 Multithreading with the Thread Class
1: Imports System.Threading 2: 3: Module Module1 4: 5: Private Value As Integer 6: 7: Sub Main() 8: 9: Dim Thread1 As Thread = New Thread(AddressOf Increment) 10: Increment2() 11: Thread1.Start() 12: Thread1.Join() 13: 14: Console.WriteLine(Value) 15: Console.ReadLine() 16: End Sub 17: 18: Public Sub Increment() 19: Dim I As Integer 20: For I = 1 To 10000000 21: AddOne() 22: Next 23: End Sub 24: 25: Public Sub Increment2() 26: Dim I As Integer 27: For I = 1 To 10000000 28: AddOne() 29: Next 30: End Sub 31: 32: Private Sub AddOne() 33: SyncLock GetType(Int32) 34: Value += 1 35: End SyncLock 36: End Sub 37: End Module
The difference between Listing 6.9 and Listing 6.12 occurs in the Main subroutine in lines 7 through 16 (Listing 6.12) and the absence of the ManualResetEvent object. Line 9 declares a new Thread object and initializes it to Increment , representing the work for the thread to complete. In this case I simply passed the address of Increment to the Thread constructor, which is treated as an implicit instance of the ThreadStart delegate. The thread is explicitly started in line 11, after Increment2 is called, and we use Thread1.Join in line 12 to block until Thread1 terminates. In Listing 6.12, Thread1.Join plays the role that ManualResetEvent plays in Listing 6.9.