So far, you've seen a number of ways to get asynchronous support for free from .NET. These techniques are ideal for making simple asynchronous calls to improve performance or to start a potentially time-consuming task without stalling the application. However, they are of much less help if you need to create intelligent, thread-aware code that communicates with other threads, runs for a long time to perform a variety of tasks, and allows variable levels of prioritization. To handle these cases, you need to take complete control of threading by using the System.Threading.Thread class and creating your own threaded objects. To create a separate thread of execution in .NET, you create a new Thread object, identify the code it should execute, and start processing. In one respect, using the Thread object is like the delegate examples we considered before because the Thread class can point to only a single method. As with the delegate examples, this can be an instance method or a shared method. Unlike with the delegate examples, however, the Thread class has one basic limitation. The method it executes asynchronously must accept no parameters and have no return value. (In other words, it must match the signature of the System.Threading.ThreadStart delegate.) The canonical threading example uses two threads that increment separate counters. To design this example, we'll use a command-line console application because the Console class is guaranteed to be thread-safe; multiple threads can call the Console.WriteLine method without causing any concurrency errors. The same is not necessarily true of the Windows user interface, where controls should be modified only from the thread that owns them. Listing 6-9 shows the full code. Listing 6-9 A console application with two worker threadsImports System.Threading Module ThreadTest Public Sub Main() ' Define threads and point to code. Dim ThreadA As New Thread(AddressOf TaskA) Dim ThreadB As New Thread(AddressOf TaskB) ' Start threads. ThreadA.Start() ThreadB.Start() ' Pause to keep window open. Console.ReadLine() End Sub Private Sub TaskA() Dim i As Integer For i = 1 To 10 Console.WriteLine("Task A at: " & i.ToString) ' Waste 1 second. WasteTime(1) Next End Sub Private Sub TaskB() Dim i As Integer For i = 1 To 10 Console.WriteLine("Task B at: " & i.ToString) ' Waste 1 second. WasteTime(1) Next End Sub Private Sub WasteTime(ByVal seconds As Integer) Dim StartTime As DateTime = DateTime.Now Do Loop Until DateTime.Now > (StartTime.AddSeconds(seconds)) End Sub End Module In this example, each thread runs a separate subroutine. This subroutine just counts from 1 to 10, delaying a second each time. The delay is implemented through another private method, WasteTime. Instead of using WasteTime, you could use the static Thread.Sleep method, which temporarily suspends your application for a number of milliseconds. However, I chose the WasteTime approach for several reasons:
The output for this program demonstrates that each thread is given equal opportunity: Task A at: 1 Task B at: 1 Task A at: 2 Task B at: 2 Task A at: 3 Task B at: 3 Task A at: 4 Task B at: 4 Task A at: 5 Task B at: 5 Task A at: 6 Task B at: 6 Task A at: 7 Task B at: 7 Task A at: 8 Task B at: 8 Task A at: 9 Task B at: 9 Task A at: 10 Task B at: 10 Table 6-2 indicates some of the properties of the Thread class. Note that you can retrieve a reference to the current thread by using the shared Thread.CurrentThread property. You can use this property in the TaskA or TaskB method to retrieve a reference to ThreadA or ThreadB, or you can use this in the Main method to retrieve a reference to the main thread of the console application, which you can configure just as you would any other thread.
Thread PrioritiesThreads might be created equal, but they don't need to stay that way. You can set the Priority property of any Thread object to one of the values from the ThreadPriority enumeration. Your choices, from highest to lowest priority, are as follows:
Thread priorities are mostly important in a relative sense. (For example, two threads at Highest priority compete for the CPU just as much as two threads at Lowest priority do.) However, when you use the higher priorities, you might steal time away from other applications and from your user interface thread, making the client computer generally less responsive. You can test out thread priorities by modifying the WasteTime method in the Console example so that the wait is determined by the number of loop iterations rather than a fixed amount of time: Private Sub WasteTime(ByVal loops As Integer) Dim i, j As Integer For i = 1 To loops For j = 1 To loops Next Next End Sub Now, if both TaskA and TaskB call WasteTime with the same sufficiently large parameter for loops (say, 10000), the one that has the highest priority will be able to wrest control and finish first. Suppose, for example, that you start the threads like this: Dim ThreadA As New Thread(AddressOf TaskA) Dim ThreadB As New Thread(AddressOf TaskB) ThreadA.Priority = ThreadPriority.AboveNormal ThreadB.Priority = ThreadPriority.Normal ThreadA.Start() ThreadB.Start() You are likely to see the following output: Task A at: 1 Task A at: 2 Task A at: 3 Task A at: 4 Task A at: 5 Task A at: 6 Task A at: 7 Task B at: 1 Task A at: 8 Task A at: 9 Task A at: 10 Task B at: 2 Task B at: 3 Task B at: 4 Task B at: 5 Task B at: 6 Task B at: 7 Task B at: 8 Task B at: 9 Task B at: 10 Caution Thread starvation describes the situation in which several threads are competing for CPU time and at least one of them is receiving insufficient resources to perform its work. This can lead to a thread that hangs around perpetually, never able to finish its task properly. To avoid thread starvation, don't use the higher thread priorities (or create a ridiculously large number of threads). Thread ManagementThe Thread class also provides some useful methods that enable you to manage threads. For example, you can suspend a thread by using the Suspend method: ThreadA.Suspend() You can also call the Join method, which works similarly to a wait handle. It blocks the calling thread and waits for the referenced thread to end, either indefinitely or according to a set timeout: ' Wait up to 10 seconds. ThreadA.Join(TimeSpan.FromSeconds(10)) ' Perform a bitwise comparison to see if the ThreadState ' includes Stopped. If (ThreadA.ThreadState And ThreadState.Stopped) = _ ThreadState.Stopped Then ' The thread has completed. Else ' After 10 seconds, the thread is still running. End If You can also kill a thread by using the Abort method. This throws a ThreadAbortException, which the thread can catch and use to clean up its resources before ending. However, the ThreadAbortException cannot be suppressed even if your thread catches it, it will be thrown again as necessary until the thread finally gives in and terminates. You can test this by adding exception-handling code to the TaskA and TaskB methods in the console example, as shown in Listing 6-10. Listing 6-10 Handling the ThreadAbortExceptionPrivate Sub TaskA() Try Dim i As Integer For i = 1 To 10 Console.WriteLine("Task A at :" & i.ToString) ' Waste 1 second. WasteTime(10000) Next Catch err As ThreadAbortException Console.WriteLine("Aborting Task A.") End Try End Sub To see this error in action, abort the thread shortly after starting it: ThreadA.Start() ThreadB.Start() ThreadA.Abort() The output for this example is shown here: Task B at: 1 Task A at: 1 Task B at: 2 Aborting Task A. Task B at: 3 Task B at: 4 Task B at: 5 Task B at: 6 Task B at: 7 Task B at: 8 Task B at: 9 Task B at: 10 When you call the Abort method, the thread is not guaranteed to abort immediately (or at all). If the thread handles the ThreadAbortException but continues to perform work (for example, an infinite loop) in the Catch or Finally error-handling block, for instance, the thread might be stalled indefinitely. The ThreadAbortException is thrown again only when the error-handling code ends. Even if a thread is well behaved, calling Abort only fires the exception the thread might still take some time to handle the error. To ensure that a thread is terminated before continuing, it's recommended that you call Join on the thread after calling Abort. ThreadA.Abort() ThreadA.Join() Also note that if Abort is called on a thread that has not yet started, the thread aborts immediately when it is started. If Abort is called on a thread that has been suspended, is sleeping, or is blocked, the thread is resumed or interrupted as needed automatically, and then it's aborted. When we look at custom threaded objects in the next section, we'll consider less invasive ways to tell a thread to stop executing. Table 6-3 lists some of the most import methods of the Thread class. Note that these methods are written as though they act instantaneously, although this is not technically the case. For example, the Thread.Start method schedules a thread to be started. It won't actually begin until several time slices later, when the Windows operating system brings it to life.
Thread DebuggingWhen you debug with threads, it's imperative that you know which thread is executing a given segment of code. You can make this determination programmatically by using Thread.CurrentThread to retrieve a reference to the currently executing thread. You should assign every thread in your program a unique string name by using the Thread.Name property, as shown in Listing 6-11. The Thread.Name property is "write once," so you can set it, but you can't modify it later. Listing 6-11 Identifying threadsDim ThreadA As New Thread(AddressOf TaskA) Dim ThreadB As New Thread(AddressOf TaskB) Thread.CurrentThread.Name = "Console Main" ThreadA.Name = "Task A" ThreadB.Name = "Task B" ThreadA.Start() ThreadB.Start() You can then quickly test the current thread while debugging by using a command such as this: ' You can also use the less invasive Debug.WriteLine() method. MessageBox.Show(Thread.CurrentThread.Name) This code can prove extremely useful. In a complex multithreaded application, it's quite common to misidentify which thread is actually executing at a given point in time. This can lead to synchronization problems. If you are using Visual Studio .NET, you have the benefit of the IDE's integrated support for debugging threads. This is a dramatic departure from earlier versions of Visual Basic, which can debug multithreaded applications only in an artificial single-threaded mode. The Visual Studio .NET debugging tools include a Threads window (Choose Debug, Windows, Threads to display it). This window shows all the currently executing threads in your program, identifies what code the thread is running (in the Location column), and indicates the thread that currently has the processor's attention with a yellow arrow (as shown in Figure 6-5). Figure 6-5. Debugging threads in Visual Studio .NET
If you pause your program's execution, you can even use the Threads window to set the active thread, freeze a thread so that it won't be available, or thaw a previously frozen thread. |