Controlling Thread Execution

I l @ ve RuBoard

All threads have a life cycle. Each thread is created, executes, and dies at some point. Understanding how to manipulate threads through this cycle is essential. Each state has certain implications for the system and thread. Understanding how these states affect your thread and the application that it executes within can make the difference between success and disaster.

The ThreadState Property and the Life Cycle of a Thread

The Thread class provides an instance property called ThreadState that exposes the current state of a thread. The type of this property is the ThreadState enumeration, which is defined in the System.Threading namespace. Each state listed in the ThreadState enumeration corresponds to a specific point in a thread's life cycle. Table 3-1 describes these states.

Table 3-1. ThreadState Enumeration Values

Member

Description

Aborted

The thread is in the Stopped state.

AbortRequested

The Thread.Abort method has been invoked on the thread, but the thread has not yet received the pending System.Threading.ThreadAbortException that will attempt to terminate it.

Running

The thread has been started. It is not blocked, and no ThreadAbortException is pending.

Stopped

The thread has stopped.

Suspended

The thread has been suspended.

SuspendRequested

The thread is being asked to suspend.

Unstarted

The Thread.Start method has not been invoked on the thread.

WaitSleepJoin

The thread is blocked as a result of a call to Wait , Sleep , or Join .

To put this discussion in context, Figure 3-2 shows the thread states in action. You can see how the thread goes from its initial "unstarted" state through running, suspension, and eventual death ( Aborted or Stopped ).

Figure 3-2. The life cycle of a managed thread.

graphics/f03pn02.jpg

Now let's delve a little deeper into what the individual states really mean and what implications they have for threads and for the system.

Unstarted

This is the initial state of all managed threads. When a thread is in the Unstarted state, it has never been run. At this point, the thread is not considered active. Although it consumes memory, which happens as soon as the thread object is constructed , it does not consume any additional processing resources until it is started. Once a thread has been started, it can never return to this state.

You can have a virtually indefinite number of thread objects in your application that are in the Unstarted state. In other words, the operating system places no limitation on the number of thread objects as long as the threads have not been started. The number of Unstarted threads is limited by the total amount of memory available. Each Unstarted Thread object consumes a small amount of memory, which merely contains the type information, the ThreadStart delegate for the thread's main method, and some other minor details.

Once you start the thread, things get more complicated. Your thread not only starts to consume processor time, but more memory is allocated to store the thread's state and other relevant information. Ultimately, there is a limit on the number of active threads available to a Windows system. The magic here number is 2000. If you attempt to start any additional active threads once you've reached this limit, you'll run into problems. Thankfully, this is a fairly rare problem because most well-designed applications don't create hundreds, let alone thousands, of active threads.

Note

Don't confuse the limit on the number of active threads with a limitation on your ability to start threads. As long as the total active thread count stays below 2000, you can create and start as many new threads as you like. (There are reasons why you'll want to keep the number of active threads to a minimum, but we'll discuss them later in this chapter when we discuss thread pools.


Running

The Running state indicates that the thread is active and is executing its main routine. If a thread looks at its own ThreadState , it will almost always return Running . This should be fairly obvious: the thread has to execute the statement, so it must be running. In more rare cases, ThreadState might return Abort ­Requested , which indicates that another thread has tried to abort the current thread. For the most part, though, a thread is rarely interested in its own state; it is more for the information of other threads.

If a thread evaluates another thread's state, it might return Running . On a single-processor machine, this result might seem counterintuitive. After all, only a single thread can be executing at any given time. But ultimately, Running does not mean "executing." A thread that is in the Running state might or might not be executing. What Running means, more that anything else, is that the thread is in an active state ”it has been given execution time by the processor, but it might not be the currently executing thread.

Suspended

The Suspended state indicates that the thread has been started but is not currently active. This thread will not execute, under any circumstances, until it is told to resume by another thread.

WaitSleepJoin

The WaitSleepJoin state is a kind of catch-all thread state. It represents three distinct mechanisms but a single result. The thread is blocked, pending a specific event. The three possibilities are as follows :

  • The thread is waiting for one or more objects ( Wait ).

  • The thread is sleeping for a specific duration ( Sleep ).

  • The thread is waiting on another thread's completion ( Join ).

You can make a thread wait for one or more objects by using the synchronization constructs described in the section titled "Thread Synchronization" later in this chapter. To make a thread sleep for a specific duration or wait for another thread's completion, you can use the Sleep and Join methods of the Thread class. The bottom line is that a thread cannot cause another thread to enter this state. A thread can only enter this state at its own behest.

Stopped

A thread's ThreadState is the ThreadState . Stopped value when the method pointed to by the thread's ThreadStart delegate returns. From the thread's perspective, when that routine returns, everything it was created for has been completed. The Stopped state, like the Aborted state, indicates that the thread has finished executing. Once a thread is in the Stopped state, it can not be restarted. It is, for all intents and purposes, dead. The Stopped state indicates that the thread completed executing in a fairly mundane fashion ”it was simply done.

Aborted

An aborted thread is a thread that has terminated in an abnormal fashion. The Aborted state, like the Stopped state, indicates that the thread has finished executing. Once a thread is in the Aborted state, it cannot be restarted. It is, for all intents and purposes, dead. If a thread is in the Aborted state, its method pointed to by the thread's ThreadStart delegate is exiting in an abnormal fashion. This is not necessarily a bad thing, but it does mean that the thread was terminated due to a cause other than simply returning at the end of a task. Once the thread has been completely terminated, the thread will be in the Stopped state.

Before we get into how to manipulate the threads using these states, we need a little information on how to access a thread reference.

Referencing the Current Thread

It is often necessary for a thread to obtain a reference to itself. Thanks to the .NET Framework, instead of having to pass around a reference to a thread variable, you can use a Shared property of the Thread class called Thread.CurrentThread :

 PublicSharedReadOnlyPropertyCurrentThreadAsThread 

This property always returns a reference to the current thread. This allows you to write generic methods that can always have access to the thread they're running on. As you read about the thread control methods in the following section, keep in mind that you can always gain access to and control over the current thread in any part of your code. Accessing different threads is another matter, but accessing the current thread for the sake of control is about as simple as it could be.

Thread Control Methods

Now that I've talked about the states of a thread and how any thread can get a reference to itself, we need to look at how to coax a thread through its life cycle. The methods provided by the Thread class give you complete control over the life cycle of threads. Table 3-2 provides an overview of these methods.

Table 3-2. Thread Control Methods

Type

Method

Description

Instance

Start

Causes a thread to start running.

Shared

Sleep

Pauses the calling thread for a specified time.

Instance

Suspend

Pauses a thread when it reaches a safe point.

Instance

Abort

Stops a thread when it reaches a safe point.

Instance

Resume

Restarts a suspended thread.

Instance

Join

Causes the calling code to wait for a thread to finish. If this method is used with a timeout value, it will return True if the thread finishes in the allotted time.

Instance

Interrupt

Interrupts a thread that is in the WaitSleepJoin state.

Shared

SpinWait

Causes a thread to wait the number of times defined by the iterations parameter. This is equivalent to inserting a simple counter loop in your code.

The purpose of some of these methods is almost painfully obvious, but the in-depth discussion that follows highlights how these methods can and should be used, which is not always so obvious. An excellent example is the concept of safe points. As described by Robert Burns on MSDN, safe points are places in code where it is safe for the common language runtime (CLR) to perform automatic garbage collection. When the Abort or Suspend methods of a thread are called, the CLR analyzes the thread's code and determines an appropriate location for the thread to stop running.

Thread.Start
 PublicSubStart() 

Before a thread can do anything, its Start method must be called by another thread. This will cause the thread's ThreadState to change to the Running state. Repeated calls to Start will cause a ThreadStateException . Start can be called only once per thread and is allowed only when the thread is in the Unstarted state.

A thread can never call its own Start method because calling Start on a thread that's not in the Unstarted state will cause a ThreadStateException to be thrown. By definition, a thread can reference itself only if it has already been started, so it should never call Start on itself (unless, of course, you like catching exceptions for the heck of it).

Note

When you call the Thread.Start method, other exceptions might be thrown that have absolutely nothing to do with the current state of the thread. It is entirely possible in extreme conditions to run into an OutOfMemoryException . It is also possible for a SecurityException to occur if your current security context does not allow you to start the thread.


Thread.Sleep
 OverloadsPublicSharedSubSleep(Integer) OverloadsPublicSharedSubSleep(TimeSpan) 

When a thread is in the Running state, it can call the Sleep method of the Thread class. The Sleep method has two distinct uses. First, you can use it to put your thread into the WaitSleepJoin state for a specified period of time. By specifying an amount of time in milliseconds or specifying a TimeSpan , you cause the operating system to put the thread on an inactive list. This is the most efficient way to introduce delays because it consumes the least amount of resources and the operating system will take care of resuming your thread when the specified time period has elapsed.

Warning

Even though you specify an exact time period of inactivity, the operating system does not guarantee that it will resume your thread immediately once the period has elapsed. Neither Windows 2000 nor the older Windows-based operating systems are real-time operating systems. So it is better to think of this as the operating system promising you that it will resume your thread as soon as possible after the specified time has elapsed. Ultimately, whether a thread resumes in 100 milliseconds or115 milliseconds will not adversely affect most applications. The longer the duration specified in the Sleep call, the less important a few milliseconds of delay will generally be. You're more likely to run into problems if you have very tight timing constraints, but this situation is not very common.


The second use of the Sleep method is to surrender the thread's execution time without actually having it go into the WaitSleepJoin state. By specifying 0 milliseconds or TimeSpan.Zero , you tell the underlying operating system that you want to give other threads a chance at the processor. There are advantages to doing this, especially for long-running or processor- intensive tasks . The following example demonstrates both uses of Thread.Sleep :

 'Surrendersexecutionofthethread. 'DoesnotcausethecurrentthreadtogointotheWaitSleepJoinstate. Thread.Sleep(0) Thread.Sleep(TimeSpan.Zero) 'Surrendersexecutionofthethread. 'CausesthecurrentthreadtogointotheWaitSleepJoinstate. Thread.Sleep(100) Thread.Sleep(NewTimeSpan(0,0,0,0,100)) 

Caution

The Sleep method is different from all of the other thread control methods because both overloads are Shared . As a result, when a thread calls Thread.Sleep , it will affect only the current thread. This can cause some confusion from a code standpoint when a thread attempts to call the Sleep method on an instance of another thread object. The code will compile and it will look like you're invoking an instance method on the thread object, but you'll still be invoking the static Sleep method and you'll affect only the current thread instance, not the referenced thread instance.


Thread.SpinWait
 PublicSharedSubSpinWait(ByValiterationsAsInteger) 

SpinWait is almost the polar opposite of the Sleep method. It delays the execution of your thread for a specific number of iterations, but it does so by spinning . In other words, your thread remains executing rather than marking the thread inactive for a specific period of time. It's the same as creating your own loop that runs continuously until the counter runs out.

This does not mean that your thread will not surrender its execution time on the processor. The operating system will continue to switch between threads, but your thread will spend x amount of its execution time spinning and doing nothing productive.

Thread.Suspend
 PublicSubSuspend() 

You can call Suspend on a thread that's in the Running state. This will force the thread into the Suspended state. You can call Suspend on a thread even if that thread is already in the Suspended state. The CLR will not cause an exception. It will figure that you're getting what you want anyway, so why bother you? Calling Suspend when the thread is in any other state than Running or Suspended will cause a ThreadStateException to be generated.

If you're calling Suspend on another thread, the CLR will attempt to suspend that thread in an appropriate place. This can lead to unexpected behavior because the CLR decides where to suspend the thread and it might not happen where you expect.

If you call Suspend on the current thread, the CLR will immediately put your thread into the Suspended state. This actually provides an argument in favor of calling Suspend only on the owner thread and not on other thread instances. By doing this, you can ensure that a thread will always suspend at a predictable location instead of potentially anywhere in its code.

Thread.Resume
 PublicSubResume() 

When a thread is in the Suspended state, another thread can call its Resume method. This will cause the thread to move from the Suspended state to the Running state. If a thread is in the Suspended state, it can be resumed only by another, active thread. If no other thread has a reference to the suspended thread, the thread can never be reactivated.

Thread.Abort
 OverloadsPublicSubAbort() OverloadsPublicSubAbort(Object) 

The Abort method has some interesting behaviors. First, calling Abort causes a ThreadAbortException to be generated on the destination thread. It is up to that thread to handle the consequences of that exception, especially clean-up. To handle the ThreadAbortException , you should include exception-handling code in your thread's main Run method. This should also highlight the importance of appropriate exception handling, or at least the use of the Try Finally syntax to ensure that file handles and database connections are cleaned up reliably. To that end, your thread's Run method might look like this example:

 PrivateSubRun Try 'Doyourstuffhere CatchexAsThreadAbortException 'Dealwiththeexception Finally 'Cleanupyourresourceshere EndTry EndSub 

Warning

An issue can arise when you call the Abort method on another thread. As I mentioned, Abort causes a ThreadAbortException to be generated on the thread. The problem is that it is impossible to guarantee the state of the thread at the time Abort is called. Say, for instance, that Abort has already been called by another thread. The target thread might be in a catch clause, dealing with the exception. The new Abort will cause the catch clause to terminate prematurely, possibly leaving resources unrelinquished. Even though the CLR will attempt to make an intelligent choice about where the Abort should occur, some things are beyond its control. Bottom line: try to call Abort from within the thread you want to abort. Avoid calling Abort directly on another thread. If you need to abort the thread, try one of the signaling mechanisms detailed in "Thread Synchronization" later in this chapter.


Thread.Join
 OverloadsPublicSubJoin() OverloadsPublicFunctionJoin(Integer)AsBoolean OverloadsPublicFunctionJoin(TimeSpan)AsBoolean 

Join is the simplest synchronization construct available to developers. When you call the Join method on a thread, the calling thread will enter the WaitSleepJoin state and will stay there until the called thread has completed (is in the Stopped or Aborted state). This allows threads to block execution pending the completion of other threads in the system. The following example shows how this might be used:

 SubMain() Dimt1AsNewThread(AddressOfThread1Method) Dimt2AsNewThread(AddressOfThread2Method) 'Startboththreads t1.Start() t2.Start() 'Waitforboththreadstocomplete t1.Join() t2.Join() 'Youareguaranteedatthispointthatboththreadshavecompleted EndSub 

If a thread has already completed, calling Join will have no effect and your application will proceed as normal.

Thread.Interrupt

You can call the Interrupt method on another thread to cause it to exit the WaitSleepJoin state. If you call this method while the thread is running, the next time the referenced thread enters the WaitSleepJoin state it will be immediately set back to Running . I don't recommend this as a common practice ”it is appropriate only in the rarest of cases.

Tying It All Together

Now you've seen how to manage a thread through its life cycle. We should spend a little time talking about thread execution and how to tailor your threads to make them run more efficiently and play well with others. Consider the following method:

 PublicDoneAsBoolean=False PublicSubMyLongRunningMethod() WhileNotDone EndWhile Console.WriteLine("Done") EndSub 

This is an example of a completely horrendous programming practice. This code will cause the thread to loop interminably while checking a variable that might take minutes, seconds, or even hours to change. This will impose a heavy processing load on a system without achieving anything. Obviously, there should be a better way, and there is. You can use the Thread.Sleep method to tell the system to put your thread out of the active thread list until a certain time period has elapsed. Alternatively, you can use the Thread.Sleep method to give other threads a chance to execute (still keeping your thread active, of course). This lends itself to two scenarios. The first, like our example above, allows you to use processing resources more efficiently by telling the system how long you can wait before you need to continue. We can recast our previous example as follows:

 PublicSubMyLongRunningMethod() WhileNotDone Thread.Sleep(100)'Sleepfor~100milliseconds EndWhile Console.WriteLine("Done") EndSub 

Instead of running this thread continuously and constantly consuming processing resources, we can efficiently poll the status of the done variable 10 times a second.

Now that you know how to manage threads by encapsulating them in classes, let's look at how to make threads work together. What if they need access to a shared resource? What if there are dependencies? How can this all be managed? This is where thread synchronization comes into play.

I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

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