Introducing Microsoft .NET (Pro-Developer)

So far in this chapter we’ve seen how to use threads in a preexisting pool, and we’ve seen how to synchronize our threads so that they don’t step on each other’s feet. What we haven’t seen yet is how to create, manage, and destroy our own threads when we want to take this responsibility on ourselves. The thread pool will work for most operations most of the time, but sometimes you need customized behavior. You might need to control the number of threads running at any time, which the pool manager doesn’t let you do. Or you might need to set the priority of your threads higher or lower than normal, which the pool manager doesn’t let you do either. Or if you want your threads to interoperate with COM objects, you might want to control the COM threading apartment that they live in. All pool manager threads live nonnegotiably in the multithreaded apartment (MTA). In short, managing your own threads is one choice in the eternal trade-off of all computing— more work in return for finer-grained control.

You might want to create your own threads for finer- grained control.

The .NET Framework provides us with the capability of exerting this control when we feel it’s worth the trouble. I’ve written a sample program, shown in Figure 9-5, that demonstrates these advanced threading features. It shows a number of balls that bounce around the screen, painting streaks in various colors. It hypnotizes audiences, especially those who have already had a lot to drink. (“You are getting sleepy. You will check the ‘Excellent’ box on the evaluation form...”)

An advanced threading sample program starts here.

Figure 9-5: Complex threading sample program.

The .NET Framework represents a thread via the object class System.Threading.Thread. You will probably want to associate a thread with your own object class, thereby giving the thread data to work on. I’d have derived my own class from the .NET Framework Thread class, except the developers have made that impossible by marking the class as uninheritable. I therefore wrote my own class called BounceThreadHolder, which contains a Thread object as a member variable. You create a new Thread in the same way you create any other object in .NET, by using the new operator to call its constructor. This object’s constructor requires us to pass it a delegate pointing to the code function that we want the thread to run, just as we did for the work item in the pooled thread case. In this sample program, I pass the member function of my BounceThreadHolder class that draws bouncing balls on the screen.

You create a new System.Threading.Thread just like any other .NET object.

Every thread exists in one of a number of states reflecting its current operating status. You can read a thread’s state via the read-only property Thread.ThreadState. You change the state through the various methods that affect a thread’s operation. Every thread is created in the Unstarted state, which allows you to set up its processing environment (the member variables, data sets, and so on that you want it to use) before it goes charging off to do its work. Calling the method Thread.Start places it into the Running state, which puts it into the operating system’s ready list and lets it compete for CPU cycles. Calling the method Thread.Suspend places it into the Suspended state, in which it receives no CPU cycles until a call to its Thread.Resume method puts it back into the Running state.

A thread can exist in a variety of states.

A thread that calls Thread.Sleep, as I showed in this chapter’s first example, or blocks while waiting for a synchronization lock, as I discussed in this chapter’s second example, enters the WaitSleepJoin state. This state is similar to the Suspended state in that the thread receives no CPU cycles, but it is different in that it receives an automatic wake-up call when the sleep interval expires or the lock is released. A thread that is sleeping or waiting can also be released from its state via the method Thread.Interrupt. This causes the sleep interval to expire or the block to clear immediately. Since this is an abnormal termination of the sleep or wait, and the waiting thread may well not own the lock for which it was waiting, the system then throws an exception of type System.Threading.ThreadInterruptedException onto the thread’s stack. If you want to recover from the interruption and keep on processing, you’ll have to write code to handle this exception, figure out what caused it, and clean up as best you can.

A thread that is sleeping or waiting can be interrupted.

Each thread has its own priority level, one of five choices: Highest, AboveNormal, Normal, BelowNormal, and Lowest. The underlying operating system contains a number of other values, but these are not exposed in the .NET-managed threading environment. You’ll have to go under the hood to get your hands on these, which you really don’t want to do. Every thread is created with Normal priority. You can read or change the priority via the property Thread.Priority.

Every thread has a priority level.

The highest priority thread that’s ready, willing, and able to run gets the CPU. If you set one thread to highest priority, you’ll find that it slows down all the other threads, even the sample program’s user interface. You’ll also notice, however, that the highest priority thread doesn’t get absolutely all the CPU cycles. Every few seconds, you’ll see another bouncing ball move just a little, indicating that its thread has gotten a timeslice. Every once in a while the operating system gives a lower priority thread a temporary priority boost so that it can have at least a few CPU cycles. This is done to prevent a runaway high-priority thread from completely starving all the threads in your entire application so badly that you can’t stop it. The more your thread wants to run without blocking, the lower you want its priority to be. For example, it’s common to set the user interface thread’s priority higher than background recalculation threads. The UI thread spends most of its time blocked, waiting for user input, a state in which it doesn’t consume CPU cycles. But when it does receive a message, its higher priority lets it knock background operations out of the way quickly, providing better responsiveness to the user. When the program has finished responding to the user’s command, the UI thread blocks again, waiting in readiness for the next user command and allowing the background threads to run again. Remember, thread priorities are relative only to each other. You can’t get your work done faster by setting every thread to the highest priority, as one of my former bosses often tried to do (and, knowing this guy, probably still does).

Threads compete for CPU cycles according to their priorities.

A thread terminates when its delegate function returns. The sample program does this when you click the Die Nicely button. The user interface sets a flag in the BounceThreadHolder object, which the thread function checks on each pass through its drawing loop, returning when it finds it set to true. From outside a thread function, you can kill a thread via the method Abort. This throws an exception of type System.Threading.ThreadAbortException up the thread’s stack. Unlike most exceptions, simply catching it in a try-catch block will not stop it from terminating the thread. You have to explicitly call the method Thread.ResetAbort from within the exception handler to keep it from terminating. Once a thread is terminated, it can’t be restarted.

A thread terminates when its delegate function returns, or it’s aborted by an external method.

It is often important to clean up after threads in an orderly manner. I’ve just told you how to get rid of a thread. The method Thread.Join blocks the thread from which you call it until the thread object on which you call it terminates. As you can see in Listing 9-7, in the sample program my form calls Thead.Join on each of the bouncing ball threads in the form’s Dispose method. If I don’t do this, my form disappears while the bouncing threads continue to run, causing them to throw exceptions as they try to draw on a nonexistent window. The exception dialog boxes look terrible to a user. Calling Join on an unstarted thread causes an exception, so you can see me checking the thread’s state before I call it.

The method Thread.Join blocks the calling thread until the target thread terminates.

Listing 9-7: Code providing orderly shutdown of multithreaded app.

Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then ’ Shut down all threads nicely. Use Thread.Join to make ’ sure they have shut down before proceeding further. ’ Can’t call it on an unstarted thread. Dim i As Integer For i = 0 To 7 ThreadHolder(i).DieNicely = True If (ThreadHolder(i).Thread.ThreadState <> _ Threading.ThreadState.Unstarted) Then ThreadHolder(i).Thread.Join() End If Next <other disposal code omitted> End If MyBase.Dispose(disposing) End Sub

Категории