Custom Threading

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 threads
 Imports 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:

  • It illustrates that the Thread class initially targets one method but that this method can call any other code in your project.

  • It demonstrates that more than one thread can call the same method at the same time without incurring any problems, unless they are trying to read from or write to the same variable at once. Each thread executes WasteTime separately, and there is no shared data, so no potential problems can occur.

  • The WasteTime method doesn't actually pause the thread. From the thread's point of view, it is still at work, iterating several thousand times through a tight loop and comparing values. Even though each thread is inefficiently hogging the CPU, and neither thread is yielding for the other, the Windows operating system still ensures that they each get a chance to execute.

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.

Table 6-2. Thread Properties

Property

Description

IsAlive

Returns True unless the thread is stopped, aborted, or unstarted.

IsBackground

A thread is either a background thread or a foreground thread. Background threads are identical to foreground threads except they can't prevent a process from ending. After all the foreground threads in your application have terminated, the CLR automatically aborts all background threads that are still alive.

Name

Enables you to set a string name that identifies the thread. This is primarily useful during debugging.

Priority

One of the ThreadPriority values that indicate the scheduling priority of the thread as compared to other threads.

ThreadState

A combination of ThreadState values, which indicate whether the thread is started, running, waiting, a background thread, and so on. You can poll this property to find out when a thread has completed its work.

Thread Priorities

Threads 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:

  • Highest

  • AboveNormal

  • Normal

  • BelowNormal

  • Lowest

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 Management

The 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 ThreadAbortException
 Private 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.

Table 6-3. Thread Methods

Method

Description

Abort

Aggressively kills a thread using the ThreadAbortException.

Interrupt

If the thread is currently waiting (using synchronization code), blocked, or sleeping, this method puts it back on track.

Join

Waits until the thread terminates (or a specified timeout elapses).

ResetAbort

If the thread calls this method when handling the ThreadAbortException, the exception will not be thrown again and the thread will be allowed to continue living.

Resume

Returns a thread to life after it has been paused with the Suspend method.

Sleep

Pauses a thread for a specified number of milliseconds.

Start

Starts a thread executing for the first time. You cannot use Start to restart a thread after it ends.

Suspend

Pauses a thread for an indefinite amount of time.

Thread Debugging

When 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 threads
 Dim 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

graphics/f06dp05.jpg

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.



Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net