Threads

Threads

The .NET Framework s threading API is embodied in members of the System.Threading namespace. Chief among the namespace s members is the Thread class, which represents threads of execution. Thread implements a variety of properties and methods that enable developers to launch and manipulate concurrently running threads.

The following table lists some of Thread s public properties. I won t detail all of them now because many are formally introduced later in the chapter. Scanning the list, however, provides a feel for the innate properties of a thread as the CLR sees it. IsBackground, for example, is a read/write property that determines whether a thread is a foreground or background thread, a concept that s described in the section entitled Foreground Threads vs. Background Threads. ThreadState lets you determine the current state of a thread is it running, for example, and if so, is it blocked on a synchronization object or is it executing code? while Name allows you to assign human-readable names to the threads that you create.

Selected Public Properties of the Thread Class

Property

Description

Get

Set

Static

CurrentPrincipal

The security principal (identity) assigned to the calling thread

CurrentThread

Returns a Thread reference representing the calling thread

IsAlive

Indicates whether the thread is alive that is, has started but has not terminated

IsBackground

Indicates whether the thread is a foreground thread or a background thread (default = false)

Name

The thread s human-readable name (default = null)

Priority

The thread s priority (default = Thread Priority.Normal)

ThreadState

The thread s current state

CurrentThread is a static property that returns a Thread reference to the calling thread. It enables a thread to acquire information about itself and to change its own properties and call its own methods. If you create a thread and have a reference to it in a Thread object named thread, you can read the thread s name by invoking Name on the thread object:

string name = thread.Name;

If a thread wants to retrieve its own name, it can use CurrentThread to acquire the Thread reference that it needs:

string myname = Thread.CurrentThread.Name;

You ll use CurrentThread and other Thread properties extensively when implementing multithreaded applications.

Starting Threads

Starting a thread is simplicity itself. The following statements launch a new thread:

Thread thread = new Thread (new ThreadStart (ThreadFunc)); thread.Start ();

The first statement creates a Thread object representing the new thread and identifies a thread method the method that the thread executes when it starts. The reference to the thread method is wrapped in a ThreadStart delegate an instance of System.Threading.ThreadStart to enable the new thread to call the thread method in a type-safe manner. The second statement starts the thread running. Once running that is, once the thread s Start method is called the thread becomes alive and remains alive until it terminates. You can determine whether a thread is alive at a given point in time by reading its IsAlive property. The following if clause suspends the thread represented by thread if the thread has started but has not terminated:

if (thread.IsAlive) { thread.Suspend (); }

Note that calling Start on a Thread object does not guarantee that the thread will begin executing immediately. Technically, Start simply makes the thread eligible to be allotted CPU time. The system decides when the thread begins running and how often it s accorded processor time.

A thread method receives no parameters and returns void. It can be static or nonstatic and can be given any legal method name. Here s a thread method that counts from 1 to 1,000,000 and returns:

void ThreadFunc () { for (int i=1; i<=1000000; i++) ; }

When a thread method returns, the corresponding thread ends. In this example, the thread ends following the for loop s final iteration. IsAlive returns true while the for loop is running and false after the for loop ends and ThreadFunc returns.

Foreground Threads vs. Background Threads

The common language runtime distinguishes between two types of threads: foreground threads and background threads. An application doesn t end until all of its foreground threads have ended. It can, however, end with background threads running. Background threads are automatically terminated when the application that hosts them ends.

Whether a thread is a foreground thread or a background thread is determined by a read/write Thread property named IsBackground. The IsBackground property defaults to false, which means that threads are foreground threads by default. Setting IsBackground to true makes a thread a background thread. In the following example, a console application launches 10 threads when it s started. Each thread loops for 5 seconds. Because the application doesn t set the threads IsBackground property to true, it doesn t end until all 10 threads have run their course. This happens despite the fact that the application s primary thread the one that was started when the application was started ends immediately after launching the other threads:

using System; using System.Threading; class MyApp { static void Main () { for (int i=0; i<10; i++) { Thread thread = new Thread (new ThreadStart (ThreadFunc)); thread.Start (); } } static void ThreadFunc () { DateTime start = DateTime.Now; while ((DateTime.Now - start).Seconds < 5) ; } }

In the next example, however, the application ends almost as soon as it s started because it changes the auxiliary threads from foreground threads to background threads:

using System; using System.Threading; class MyApp { static void Main () { for (int i=0; i<10; i++) { Thread thread = new Thread (new ThreadStart (ThreadFunc)); thread.IsBackground = true; thread.Start (); } } static void ThreadFunc () { DateTime start = DateTime.Now; while ((DateTime.Now - start).Seconds < 5) ; } }

What determines whether a thread should be a foreground thread or a background thread? That s up to the application. Threads that perform work in the background and have no reason to continue running if the application shuts down should be background threads. If an application launches a thread that performs a lengthy computation, for example, making the thread a background thread enables the application to shut down while a computation is in progress without explicitly stopping the thread. Foreground threads are ideal for threads that make up the very fabric of an application. If you write a managed user interface shell and launch different threads to service the different windows that the shell displays, for example, you could use foreground threads to prevent the shell from shutting down when the thread that created the other threads ends.

Thread Priorities

Once a thread is started, the amount of processor time it s allotted is determined by the thread scheduler. When a managed application runs on a Windows machine, the thread scheduler is provided by Windows itself. On other platforms, the thread scheduler might be part of the operating system, or it might be part of the .NET Framework. Regardless of how the thread scheduler is physically implemented, you can influence how much or how little CPU time a thread receives relative to other threads in the same process by changing the thread s priority.

A thread s priority is controlled by the Thread.Priority property. Here are the priority values that the .NET Framework supports:

Priority

Meaning

ThreadPriority.Highest

Highest thread priority

ThreadPriority.AboveNormal

Higher than normal priority

ThreadPriority.Normal

Normal priority (the default)

ThreadPriority.BelowNormal

Lower than normal priority

ThreadPriority.Lowest

Lowest thread priority

A thread s default priority is ThreadPriority.Normal. All else being equal, n threads of equal priority receive roughly equal amounts of CPU time. (Note that many factors can make the distribution of CPU time uneven for example, threads blocking on message queues or synchronization objects and priority boosting by the operating system. Conceptually, however, it s accurate to say that equal threads with equal priorities will receive, on average, about the same amount of CPU time.)

You can change a thread s priority by writing to its Priority property. The following statement boosts a thread s priority:

thread.Priority = ThreadPriority.AboveNormal;

The next statement lowers the thread s priority:

thread.Priority = ThreadPriority.BelowNormal;

Raising a thread s priority increases the likelihood (but does not guarantee) that the thread will receive a larger share of CPU time. Lowering the priority means the thread will probably receive less CPU time. You should never raise a thread s priority without a compelling reason for doing so. In theory, you could starve some threads of processor time by boosting the priorities of other threads too high. You could even affect threads in other applications if those applications share an application domain with yours. That s because thread priorities are relative to all threads in the host process, not just other threads in your application domain.

You can change a thread s priority at any time during the thread s lifetime before the thread is started or after it s started and you can change it as frequently and as many times as you like. Just remember that the effect of changing thread priorities is highly platform-dependent and that it might have no effect at all on non-Windows platforms.

Suspending and Resuming Threads

The Thread class features two methods for stopping a running thread and starting it again. Thread.Suspend temporarily suspends a running thread. Thread.Resume starts it running again. Unlike the Windows kernel, the .NET Framework doesn t maintain suspend counts for individual threads. The practical implication is that if you call Suspend on a thread 10 times, one call to Resume will get it running again.

Thread also provides a static method named Sleep that a thread can call to suspend itself for a specified number of milliseconds. The following example demonstrates how Sleep might be used to drive a slide show:

while (ContinueDrawing) { DrawNextSlide (); // Draw another slide Thread.Sleep (5000); // Pause for 5 seconds and go again }

A thread can call Sleep only on itself. Any thread, however, can call Suspend on another thread. If a thread calls Suspend on itself, another thread must call Resume on it to start it running again. (Think about it!)

Terminating Threads

Windows programmers have long lamented the fact that the Windows API provides no guaranteed way for one thread to cleanly terminate another. If thread A wants to terminate thread B, it typically does so by signaling thread B that it s time to end and having thread B respond to the signal by terminating itself. This means that a developer has to include logic in thread B that checks for and responds to the signal, which complicates development and means that the timeliness of thread B s termination depends on how often B checks for A s signal.

Good news: in managed code, a thread can cleanly terminate another sort of (more on my equivocation in a moment). Thread.Abort terminates a running thread. The following statement terminates the thread represented by thread:

thread.Abort ();

How does Abort work? Since the CLR supervises the execution of managed threads, it can throw exceptions in them too. Abort throws a ThreadAbortException in the targeted thread, causing the thread to end. The thread might not end immediately; in fact, it s not guaranteed to end at all. If the thread has called out to unmanaged code, for example, and hasn t yet returned, it doesn t terminate until it begins executing managed code again. If the thread gets stuck in an infinite loop in unmanaged code outside the CLR s purview, it won t terminate at all. Hopefully, however, cases such as this will be the exception rather than the rule. In practice, calling Abort on a thread that executes only managed code kills the thread quickly.

The CLR does everything in its power to terminate an aborted thread cleanly. Sometimes, however, its best isn t good enough. Consider the case of a thread that uses a SqlConnection object to query a database. How can the thread close the connection if the CLR kills it prematurely? The answer is to close the connection in a finally block, as shown here:

SqlConnection conn = new SqlConnection ("server=localhost;database=pubs;uid=sa;pwd="); try { conn.Open (); . . . } finally { conn.Close (); }

When the CLR throws a ThreadAbortException to terminate the thread, the finally block executes before the thread ends. Closing the connection in a finally block is a good idea anyway because it ensures that the connection is closed if any kind of exception occurs. It s an especially good idea if the code is executed by a thread that might be terminated by another. Otherwise, the clean kill you intended to perform with Abort might not be so clean after all.

A thread can catch ThreadAbortExceptions with catch blocks. It cannot, however, eat the exception and prevent itself from being terminated. The CLR automatically throws another ThreadAbortException when the catch handler ends, effectively terminating the thread. A thread can prevent itself from being terminated with Thread.ResetAbort. The thread in the following example foils attempts to shut it down by calling ResetAbort in the catch block that executes when the CLR throws a ThreadAbortException:

try { . . . } catch (ThreadAbortException) { Thread.ResetAbort (); }

Assuming the thread has sufficient privilege to overrule another thread s call to Abort, execution continues following the catch block.

In practice, a thread that terminates another thread often wants to pause until the other thread has terminated. The Thread.Join method lets it do just that. The following example requests the termination of another thread and waits until it ends:

thread.Abort (); // Ask the other thread to terminate thread.Join (); // Pause until it does

Because there s no ironclad guarantee that the other thread will terminate (it could, after all, get stuck in the never-never land of unmanaged code or become entangled in an infinite loop in a finally block), Thread offers an alternative form of Join that accepts a time-out value in milliseconds:

thread.Join (5000); // Pause for up to 5 seconds

In this example, Join returns when thread ends or 5 seconds elapse, whichever comes first, and returns a Boolean indicating what happened. A return value equal to true means the thread ended, while false means the time-out interval elapsed first. The time-out interval can also be expressed as a TimeSpan value.

If It Sounds Too Good to Be True

In version 1.0 of the CLR, the ThreadAbortException mechanism for terminating threads suffers from a potentially fatal flaw. If thread B is executing code in a finally block at the exact moment that thread A calls Abort on it, the resulting ThreadAbortException causes B to exit its finally block early, possibly skipping critical clean-up code. In other words, it s still not possible to guarantee a clean kill on another thread without that thread s cooperation unless the thread is kind enough to avoid using finally blocks.

As I write this, Microsoft is investigating ways to fix this problem in a future release of the .NET Framework. Hopefully, everything I said in the previous section will be true in the near future. Meanwhile, if you need to cleanly terminate one thread from another and you can t avoid finally blocks, do it the old-fashioned way: use a ManualResetEvent or some other type of synchronization object as a signaling mechanism and have the thread that you want to kill terminate itself when the signal is given.

The Sieve and MultiSieve Applications

The sample applications in this section demonstrate basic multithreading programming techniques as well as why multithreading is sometimes useful in the first place. The first application is pictured in Figure 14-1; its source code appears in Figure 14-2. Named Sieve, it s a single-threaded Windows Forms application that uses the famous Sieve of Eratosthenes algorithm to compute the number of prime numbers between 2 and a user-specified ceiling. Clicking the Start button starts the computation rolling. The results appear in the box in the center of the form. Depending on the value that you enter in the text box in the upper right, the computation can take a long time or a short time to complete.

Because the same thread that drives the application s user interface also performs the computation, Sieve is dead to user input while it counts prime numbers. Try it. Enter a fairly large value (say, 100,000,000) into the input box and click Start. Now try to move the window. It doesn t budge. Under the hood, your attempts to move the window place messages in the thread s message queue. The messages are ignored, however, while the computation proceeds because the thread responsible for retrieving them and dispatching them to the window is busy crunching numbers. Sieve doesn t even bother to enable the Cancel button because clicking it would do nothing. Button clicks produce messages. Those messages go unanswered if the message queue isn t being serviced.

Figure 14-1

The Sieve application.

Sieve.cs

using System; using System.Drawing; using System.Windows.Forms; using System.Collections; using System.Threading; class SieveForm : Form { Label Label1; TextBox Input; TextBox Output; Button MyStartButton; Button MyCancelButton; SieveForm () { // Initialize the form's properties Text = "Sieve"; ClientSize = new System.Drawing.Size (292, 158); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; // Instantiate the form's controls Label1 = new Label (); Input = new TextBox (); Output = new TextBox (); MyStartButton = new Button (); MyCancelButton = new Button (); // Initialize the controls Label1.Location = new Point (24, 28); Label1.Size = new Size (144, 16); Label1.Text = "Number of primes from 2 to";

 Input.Location = new Point (168, 24); Input.Size = new Size (96, 20); Input.Name = "Input"; Input.TabIndex = 0; Output.Location = new Point (24, 64); Output.Size = new Size (240, 20); Output.Name = "Output"; Output.ReadOnly = true; Output.TabStop = false; MyStartButton.Location = new Point (24, 104); MyStartButton.Size = new Size (104, 32); MyStartButton.Text = "Start"; MyStartButton.TabIndex = 1; MyStartButton.Click += new EventHandler (OnStart); MyCancelButton.Location = new Point (160, 104); MyCancelButton.Size = new Size (104, 32); MyCancelButton.Text = "Cancel"; MyCancelButton.TabIndex = 2; MyCancelButton.Enabled = false; // Add the controls to the form Controls.Add (Label1); Controls.Add (Input); Controls.Add (Output); Controls.Add (MyStartButton); Controls.Add (MyCancelButton); } void OnStart (object sender, EventArgs e) { // Get the number that the user typed int MaxVal = 0; try { MaxVal = Convert.ToInt32 (Input.Text); } catch (FormatException) { MessageBox.Show ("Please enter a number greater than 2"); return; } if (MaxVal < 3) { MessageBox.Show ("Please enter a number greater than 2"); return; } // Prepare the UI MyStartButton.Enabled = false; Output.Text = ""; Refresh (); // Perform the computation int count = CountPrimes (MaxVal); // Update the UI Output.Text = count.ToString (); MyStartButton.Enabled = true; } int CountPrimes (int max) { BitArray bits = new BitArray (max + 1, true); int limit = 2; while (limit * limit < max) limit++; for (int i=2; i<=limit; i++) { if (bits[i]) { for (int k=i + i; k<=max; k+=i) bits[k] = false; } } int count = 0; for (int i=2; i<=max; i++) { if (bits[i]) count++; } return count; } static void Main () { Application.Run (new SieveForm ()); } }

Figure 14-2

Single-threaded Sieve application.

The MultiSieve application listed in Figure 14-3 solves the user interface problem by spawning a separate thread to count primes. On the outside, the two applications are identical save for the names in their title bars. On the inside, they re very different. MultiSieve s Start button creates a thread, converts it into a background thread, and starts it running:

SieveThread = new Thread (new ThreadStart (ThreadFunc)); SieveThread.IsBackground = true; SieveThread.Start ();

The new thread performs the prime number computation and displays the results in the window:

int count = CountPrimes (MaxVal); Output.Text = count.ToString ();

See for yourself that this makes the program more responsive by dragging the window around the screen while the background thread counts primes. Dragging works because the application s primary thread is no longer tied up crunching numbers.

Clicking the Start button also causes the Cancel button to become enabled. If the user clicks Cancel while the computation is ongoing, OnCancel cancels the computation by aborting the background thread. Here s the relevant code:

SieveThread.Abort ();

OnCancel also calls Thread.Join to wait for the thread to terminate, even though no harm would occur if a new computation was started before the previous computation ends.

MultiSieve sets the computational thread s IsBackground property to true for a reason: to allow the user to close the application even if the computational thread is busy. If that thread were a foreground thread, additional logic would be required to shut down immediately if the close box in the window s upper right corner was clicked while the thread was running. As it is, no such logic is required because the background thread terminates automatically.

Calling a method from an auxiliary thread is one way to prevent an application s primary thread from blocking while waiting for a method call to return. But there s another way, too. You can use asynchronous delegates to call methods asynchronously that is, without blocking the calling threads. An asynchronous call returns immediately; later, you make another call to complete the call and retrieve the results. Asynchronous delegates obviate the need to spin up background threads for the sole purpose of making method calls and are exceedingly easy to use. They work perfectly well with local objects, but the canonical use for them is to call remote objects objects that live outside the caller s application domain (often on entirely different machines). Asynchronous delegates are introduced in Chapter 15.

MultiSieve.cs

using System; using System.Drawing; using System.Windows.Forms; using System.Collections; using System.Threading; class SieveForm : Form { Label Label1; TextBox Input; TextBox Output; Button MyStartButton; Button MyCancelButton; Thread SieveThread; int MaxVal; SieveForm () { // Initialize the form's properties Text = "MultiSieve"; ClientSize = new System.Drawing.Size (292, 158); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; // Instantiate the form's controls Label1 = new Label (); Input = new TextBox (); Output = new TextBox (); MyStartButton = new Button (); MyCancelButton = new Button (); // Initialize the controls Label1.Location = new Point (24, 28); Label1.Size = new Size (144, 16); Label1.Text = "Number of primes from 2 to"; Input.Location = new Point (168, 24); Input.Size = new Size (96, 20); Input.Name = "Input"; Input.TabIndex = 0; Output.Location = new Point (24, 64); Output.Size = new Size (240, 20);

 Output.Name = "Output"; Output.ReadOnly = true; Output.TabStop = false; MyStartButton.Location = new Point (24, 104); MyStartButton.Size = new Size (104, 32); MyStartButton.Text = "Start"; MyStartButton.TabIndex = 1; MyStartButton.Click += new EventHandler (OnStart); MyCancelButton.Location = new Point (160, 104); MyCancelButton.Size = new Size (104, 32); MyCancelButton.Text = "Cancel"; MyCancelButton.TabIndex = 2; MyCancelButton.Enabled = false; MyCancelButton.Click += new EventHandler (OnCancel); // Add the controls to the form Controls.Add (Label1); Controls.Add (Input); Controls.Add (Output); Controls.Add (MyStartButton); Controls.Add (MyCancelButton); } void OnStart (object sender, EventArgs e) { // Get the number that the user typed try { MaxVal = Convert.ToInt32 (Input.Text); } catch (FormatException) { MessageBox.Show ("Please enter a number greater than 2"); return; } if (MaxVal < 3) { MessageBox.Show ("Please enter a number greater than 2"); return; } // Prepare the UI MyStartButton.Enabled = false; MyCancelButton.Enabled = true; Output.Text = ""; // Start a background thread to count prime numbers SieveThread = new Thread (new ThreadStart (ThreadFunc)); SieveThread.IsBackground = true; SieveThread.Start (); } void OnCancel (object sender, EventArgs e) { if (SieveThread != null && SieveThread.IsAlive) { // Terminate the background thread SieveThread.Abort (); // Wait until the thread terminates SieveThread.Join (); // Restore the UI MyStartButton.Enabled = true; MyCancelButton.Enabled = false; SieveThread = null; } } int CountPrimes (int max) { BitArray bits = new BitArray (max + 1, true); int limit = 2; while (limit * limit < max) limit++; for (int i=2; i<=limit; i++) { if (bits[i]) { for (int k=i + i; k<=max; k+=i) bits[k] = false; } } int count = 0; for (int i=2; i<=max; i++) { if (bits[i]) count++; } return count; } void ThreadFunc () { // Do the computation int count = CountPrimes (MaxVal); // Update the UI Output.Text = count.ToString (); MyStartButton.Enabled = true; MyCancelButton.Enabled = false; } static void Main () { Application.Run (new SieveForm ()); } }

Figure 14-3

Multithreaded Sieve application.

Timer Threads

The System.Threading namespace s Timer class enables you to utilize timer threads threads that call a specified method at specified intervals. To demonstrate, the console application in the following code listing uses a timer thread to alternately write Tick and Tock to the console window at 1-second intervals:

using System; using System.Threading; class MyApp { static bool TickNext = true; static void Main () { Console.WriteLine ("Press Enter to terminate..."); TimerCallback callback = new TimerCallback (TickTock); Timer timer = new Timer (callback, null, 1000, 1000); Console.ReadLine (); } static void TickTock (object state) { Console.WriteLine (TickNext ? "Tick" : "Tock"); TickNext = ! TickNext; } }

In this example, the first callback comes after 1000 milliseconds have passed (the third parameter passed to Timer s constructor); subsequent callbacks come at 1000-millisecond intervals (the fourth parameter). Callbacks come on threads created and owned by the system, so they execute asynchronously with respect to other threads in the application (including the primary thread). You can reprogram the callback intervals while a timer thread is running with a call to Timer.Change. You can also use the constructor s second parameter to pass data to the callback method. A reference provided there becomes the callback method s one and only parameter: state.

Don t expect the callback method to be called at exactly the intervals you specify. Windows is not a real-time operating system, nor is the CLR a real-time execution engine. Timer callbacks occur at about the intervals you specify, but because of the vagaries of thread scheduling, you can t count on millisecond accuracy. Nonetheless, timer threads are extraordinarily useful for performing tasks at (approximately) regular intervals and doing so asynchronously with respect to other threads. A classic use for timer threads in a GUI application is driving a simulated clock. Rather than advance the second hand one stop in each callback, the correct approach is to check the wall-clock time in each callback and update the on-screen clock accordingly. That way the precise timing of the callbacks is unimportant.



Programming Microsoft  .NET
Applied MicrosoftNET Framework Programming in Microsoft Visual BasicNET
ISBN: B000MUD834
EAN: N/A
Year: 2002
Pages: 101

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