< Day Day Up > |
The asynchronous techniques discussed in the previous section work best when an application or component's operations can be run on independent threads that contain all the data and methods they need for execution and when the threads have no interest in the state of other concurrently running threads. The asynchronous techniques do not work as well for applications running concurrent threads that do have to share resources and be aware of the activities of other threads. The challenge is no longer to determine when a thread finishes executing, but how to synchronize the activities of multiple threads so they do not corrupt each other's work. It's not an easy thing to do, but it can greatly improve a program's performance and a component's usability. In this section, we'll look at how to create and manage threads running concurrently. This serves as a background for the final section that focuses on synchronization techniques used to ensure thread safety. Creating and Working with ThreadsAn application can create a thread, identify it, set its priority, set it to run in the background or foreground, coordinate its activities with other threads, and abort it. Let's look at the details. The Current ThreadAll code runs on either the primary thread or a worker thread that is accessible through the CurrentThread property of the Thread class. We can use this thread to illustrate some of the selected Thread properties and methods that provide information about a thread: Thread currThread = Thread.CurrentThread; Console.WriteLine(currThread.GetHashCode()); Console.WriteLine(currThread.CurrentCulture); // en-US Console.WriteLine(currThread.Priority); // normal Console.WriteLine(currThread.IsBackground); // false Console.WriteLine(AppDomain.GetCurrentThreadId()); // 3008 Thread.GetHashCode overrides the Object.GetHashCode method to return a thread ID. The thread ID is not the same as the physical thread ID assigned by the operating system. That ID, which .NET uses internally to recognize threads, is obtained by calling the AppDomain.GetCurrentThreadID method. Creating ThreadsTo create a thread, pass its constructor a delegate that references the method to be called when the thread is started. The delegate parameter may be an instance of the ThreadStart or ParameterizedTheadStart delegate. The difference in the two is their signature: ThreadStart accepts no parameters and returns no value; ParameterizedThreadStart accepts an object as a parameter, which provides a convenient way to pass data to thread. After the thread is created, its Start method is invoked to launch the thread. This segment illustrates how the two delegates are used to create a thread: Thread newThread = new Thread(new ThreadStart(GetBMI)); newThread.Start(); // Launch thread asynchronously Thread newThread = new Thread(new ParameterizedThreadStart(GetBMI)); newThread.Start(40); // Pass data to the thread To demonstrate thread usage, let's modify the method to calculate a BMI value (see Listing 13-2) to execute on a worker thread (Listing 13-4). The weight and height values are passed in an array object and extracted using casting. The calculated value is exposed as a property of the BMI class. Listing 13-4. Passing Parameters to a Thread's Method // Create instance of class and set properties BMI b = new BMI(); decimal[] bmiParms = { 168M, 73M }; // Weight and height // Thread will execute method in class instance Thread newThread = new Thread( new ParameterizedThreadStart(b.GetBMI)); newThread.Start(bmiParms); // Pass parameter to thread Console.WriteLine(newThread.ThreadState); // Unstarted Console.WriteLine(b.Bmi); // Use property to display result // Rest of main class ... } public class BMI { private decimal bmival; public void GetBMI(object obj) { decimal[] parms= (decimal[])obj; decimal weight = parms[0]; decimal height = parms[1] ; // Simulate delay to do some work Thread.Sleep(1000); // Build in a delay of one second bmival = (weight * 703 * 10/(height*height))/10 ; } // Property to return BMI value public decimal Bmi { get {return bmival; }} } In reality, the method GetBMI does not do enough work to justify running on a separate thread; to simulate work, the Sleep method is called to block the thread for a second before it performs the calculation. At the same time, the main thread continues executing. It displays the worker thread state and then displays the calculated value. However, this logic creates a race condition in which the calling thread needs the worker thread to complete the calculation before the result is displayed. Because of the delay we've included in GetBMI, that is unlikely and at best unpredictable. One solution is to use the THRead.Join method, which allows one thread to wait for another to finish. In the code shown here, the Join method blocks processing on the main thread until the thread running the GetBMI code ends execution: newThread.Start(); Console.WriteLine(newThread.ThreadState); newThread.Join(); // Block until thread finishes Console.WriteLine(b.bmi); Note that the most common use of Join is as a safeguard to ensure that worker threads have terminated before an application is shut down. Aborting a ThreadAny started thread that is not in a suspended state can be requested to terminate using the THRead.Abort method. Invoking this method causes a THReadAbortException to be raised on its associated thread; thus, the code running the thread must implement the proper exception handling code. Listing 13-5 shows the code to implement both the call and the exception handling. The calling method creates a thread, sleeps for a second, and then issues an Abort on the worker thread. The parameter to this command is a string that can be displayed when the subsequent exception occurs. The Join command is then used to wait for the return after the thread has terminated. The method running on the worker thread loops until it is aborted. It is structured to catch the THReadAbortException raised by the Abort command and print the message exposed by the exception's ExceptionState property. Listing 13-5. How to Abort a Threadusing System; using System.Threading; class TestAbort { public static void Main() { Thread newThread = new Thread(new ThreadStart(TestMethod)); newThread.Start(); Thread.Sleep(1000); if(newThread.IsAlive) { Console.WriteLine("Aborting thread."); // (1) Call abort and send message to Exception handler newThread.Abort("Need to close all threads."); // (2) Wait for the thread to terminate newThread.Join(); Console.WriteLine("Shutting down."); } } static void TestMethod() { try { bool iloop=true; while(iloop) { Console.WriteLine("Worker thread running."); Thread.Sleep(500); // Include next statement to prevent abort // iloop=false; } } catch(ThreadAbortException abortException) { // (3) Display message sent with abort command Console.WriteLine((string)abortException.ExceptionState); } } } The Abort command should not be regarded as a standard way to terminate threads, any more than emergency brakes should be regarded as a normal way to stop a car. If the thread does not have adequate exception handling, it will fail to perform any necessary cleanup actions leading to unpredictable results. Alternate approaches to terminating a thread are presented in the section on thread synchronization. Multithreading in ActionTo gain insight into thread scheduling and performance issues, let's set up an application to create multiple threads that request the same resources. Figure 13-5 illustrates our test model. The server is a class that loads images from its disk storage on request and returns them as a stream of bytes to a client. The client spins seven threads with each thread requesting five images. To make things interesting, the threads are given one of two different priorities. Parenthetically, this client can be used for stress testing because the number of threads and images requested can be set to any value. Figure 13-5. Multithreading used to return images as a byte arrayThe ImageServer class shown in Listing 13-6 uses the Stream class to input the requested image file, write it into a memory stream, and convert this stream to an array of bytes that is returned to the client. Note that any exceptions thrown in the server are handled by the client code. Listing 13-6. Class to Return Imagespublic class ImageServer { public static byte[] GetMovieImage(string imageName, int threadNum ) { // Returns requested image to client as a series of bytes, // and displays thread number of calling thread. int imgByte; imageName= "c:\\images\\"+imageName; // If file not available exception is thrown and caught by // client. FileStream s = File.OpenRead(imageName); MemoryStream ms = new MemoryStream(); while((imgByte =s.ReadByte())!=-1) { ms.WriteByte(((byte)imgByte)); } // Display order in which threads are processed Console.WriteLine("Processing on Thread: {0}",threadNum); return ms.ToArray(); } } The code shown in Listing 13-7 uses the techniques described earlier to create seven threads that call the static FetchImage method on the ImageServer class. The threads are alternately assigned a priority of Lowest or AboveNormal, so that we can observe how their scheduling is affected by priority. Each thread makes five requests for an image from the server by calling its GetMovieImage method. These calls are inside an exception handling block that displays any exception message originating at the server. Listing 13-7. Using Multithreading to Retrieve Imagesusing System; using System.Collections; using System.Threading; namespace ThreadExample { class SimpleClient { static void Main(string[] args) { Threader t=new Threader(); } } class Threader { ImageServer server; public Threader(){ server = new ImageServer(); Object used to fetch images StartThreader(); } public void StartThreader() { // Create seven threads to retrieve images for (int i=0; i<7; i++) { // (1) Create delegate ThreadStart threadStart = new ThreadStart(FetchImage); // (2) Create thread Thread workerThread = new Thread(threadStart); // (3) Set two priorities for comparison testing if( i % 2 == 1) workerThread.Priority = ThreadPriority.Lowest; else workerThread.Priority = ThreadPriority.AboveNormal; // (4) Launch Thread workerThread.Start(); } } public void FetchImage() { // Display Thread ID Console.WriteLine( "Spinning: "+Thread.CurrentThread.GetHashCode()); string[] posters = {"afi1.gif","afi2.gif", "afi4.gif", "afi7.gif","afi89gif"}; // Retrieve five images on each thread try { for (int i=0;i<5;i++) { byte[] imgArray = server.GetMovieImage( posters[i], Thread.CurrentThread.GetHashCode()); MemoryStream ms = new MemoryStream(imgArray); Bitmap bmp = new Bitmap(ms); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } // FetchImage } // Threader // ImageServer class goes here... } // ThreadExample Because GetMovieImage prints the hash code associated with each image it returns, we can determine the order in which thread requests are fulfilled. Figure 13-6 shows the results of running this application. The even-numbered threads have the higher priority and are processed first in round-robin sequence. The lower priority threads are then processed with no interleaved execution among the threads. Figure 13-6. Effect of thread priority on thread executionThe program was run several times to test the effects of varying the number of images requested. In general, the same scheduling pattern shown here prevails, although as more images are requested the lower priority threads tend to run in an interleaved fashion. Using the Thread PoolCreating threads can be a relatively expensive process, and for this reason, .NET maintains a collection of predefined threads known as a thread pool. Threads in this pool can be acquired by an application and then returned for reuse when they have finished running. Recall from Section 13.2 that when a program uses asynchronous delegate invocation to create a thread, the thread actually comes from the thread pool. An application can also access this pool directly by following two simple steps. The first step is to create a WaitCallback delegate that points to the method to be executed by the thread. This method must, of course, match the signature of the delegate, which takes one object parameter and returns no value. Next, the QueueUserWorkItem static method of the ThreadPool class is called. The first parameter to this method is the delegate; it also takes an optional second parameter that can be used to pass information to the method called by the delegate. To illustrate, let's alter the previous example to acquire threads from a pool rather than creating them explicitly. An object parameter must be added to FetchImage so that it matches the delegate signature. Then, replace the code to create threads with these two statements: WaitCallback callBack = new WaitCallback(FetchImage); ThreadPool.QueueUserWorkItem(callBack, "image returned"); This places a request on the thread pool queue for the next available thread. The first time this runs, the pool must create a thread, which points out an important fact about the thread pool. It contains no threads when it is created, and handles all thread requests by either creating a thread or activating one already in the pool. The pool has a limit (25) on the number of threads it can hold, and if these are all used, a request must wait for a thread to be returned. You can get some information about the status of the thread pool using the GetAvailableThreads method: int workerThreads; int asyncThreads; ThreadPool.GetAvailableThreads(out workerThreads, out asyncThreads); This method returns two values: the difference between the maximum number of worker and asynchronous threads the pool supports, and the number of each currently active. Thus, if three worker threads are being used, the workerThreads argument has a value of 22. The thread pool is most useful for applications that repeatedly require threads for a short duration. For an application that requires only a few threads that run simultaneously, the thread pool offers little advantage. In fact, the time required to create a thread and place it in the thread pool exceeds that of explicitly creating a new thread. Core Note Threads exist in the thread pool in a suspended state. If a thread is not used in a given time interval, it destroys itself freeing its resources. TimersMany applications have a need to perform polling periodically to collect information or check the status of devices attached to a port. Conceptually, this could be implemented by coupling a timer with a delegate: The delegate handles the call to a specified method, while the timer invokes the delegate to place the calls at a specified interval. In .NET, it is not necessary to write your own code to do this; instead, you can use its prepackaged Timer classes. Let's look at a couple of the most useful ones: System.Timers.Timer and Windows.Forms.Timer. The former is for general use, whereas the latter is designed for Windows Forms applications. System.Timers.Timer ClassTo use the Timer class, simply register an event handling method or methods with the class's Elapsed event. The signature of the method(s) must match that of the ElapsedEventHandler delegate associated with the event: public delegate void ElapsedEventHandler(object sender, ElapsedEventArgs e); The Elapsed event occurs at an interval specified by the Timer.Interval property. A thread from the thread pool is used to make the call into the event handler(s). This code segment demonstrates how the Timer causes a method to be called every second: using System; using System.Timers; public class TimerTest { public static void Main() { SetTimer t = new SetTimer(); t.StartTimer(); } } class SetTimer { int istart; public void StartTimer() { istart= Environment.TickCount; //Time when execution begins Timer myTimer = new myTimer(); myTimer.Elapsed+=new ElapsedEventHandler(OnTimedEvent); myTimer.Interval=1000; // 1000 milliseconds myTimer.Enabled=true; Console.WriteLine("Press any key to end program."); Console.Read(); myTimer.Stop(); } // Timer event handler private void OnTimedEvent(object source, ElapsedEventArgs e) { Console.WriteLine("Elapsed Time: {0}", Environment.TickCount-istart); } } System.Windows.Forms.Timer ClassWe can dispense with a code example of this class, because its implementation parallels that of the Timers.Timer class, with two differences: It uses a Tick exception rather than Elapsed, and it uses the familiar EventHandler as its delegate. However, the feature that distinguishes it from the other Timer class is that it does not use a thread from the thread pool to call a method. Instead, it places calls on a queue to be handled by the main UI thread. Except for situations where the time required by the invoked method may make the form unresponsive, a timer is preferable to using threading. It eliminates the need to deal with concurrent threads and also enables the event handler to directly update the form's controls something that cannot be done by code on another thread. |
< Day Day Up > |