Multithreading with GUIs
The nature of multithreaded programming prevents you from knowing exactly when a thread will execute. Windows form components are not thread safeif multiple threads manipulate a Windows GUI component, the results may not be correct. To ensure that threads manipulate GUI components in a thread-safe manner, all interactions with GUI components should be performed by the User Interface thread (also known as the UI thread)the thread that creates and maintains the GUI. Class Control provides method Invoke to help with this process. Method Invoke specifies GUI processing statements that the UI thread should execute. The method receives as its arguments a delegate representing a method that will modify the GUI and an optional array of objects representing the parameters to the method. At some point after Invoke is called, the UI thread will execute the method represented by the delegate, passing the contents of the object array as the method's arguments.
Our next example (Figs. 15.13 and 15.14) uses separate threads to modify the content displayed in a Windows GUI. This example also demonstrates how to use thread synchronization to suspend a thread (i.e., temporarily prevent it from executing) and to resume a suspended thread. The GUI for the application contains three Labels and three CheckBoxes. Each thread in the program displays random characters in a particular Label. The user can temporarily suspend a thread by clicking the appropriate CheckBox and can resume the thread's execution by clicking the same CheckBox again.
Class RandomLetters (Fig. 15.13) contains method GenerateRandomCharacters (lines 3358), which takes no arguments and returns void. Line 36 uses static Thread property currentThread to determine the currently executing thread, then uses the thread's Name property to get the thread's name. Each executing thread is assigned a name that includes the number of the thread in the Main method (see the output of Fig. 15.14). Lines 3857 are an infinite loop. [Note: In earlier chapters, we have said that infinite loops are bad programming because the application will not terminate. In this case, the infinite loop is in a separate thread from the main thread. When the application window is closed in this example, all the threads created by the main thread are closed as well, including threads (such as this one) that are executing infinite loops.] In each iteration of the loop, the thread sleeps for a random interval from 0 to 1 second (line 41).
Figure 15.13. Class RandomLetters outputs random letters and can be suspended.
1 // Fig. 15.13: RandomLetters.cs 2 // Writes a random letter to a label 3 using System; 4 using System.Windows.Forms; 5 using System.Drawing; 6 using System.Threading; 7 8 public class RandomLetters 9 { 10 private static Random generator = new Random(); // for random letters 11 private bool suspended = false; // true if thread is suspended 12 private Label output; // Label to display output 13 private string threadName; // name of the current thread 14 15 // RandomLetters constructor 16 public RandomLetters( Label label ) 17 { 18 output = label; 19 } // end RandomLetters constructor 20 21 // delegate that allows method DisplayCharacter to be called 22 // in the thread that creates and maintains the GUI 23 private delegate void DisplayDelegate( char displayChar ); 24 25 // method DisplayCharacter sets the Label's Text property 26 private void DisplayCharacter( char displayChar ) 27 { 28 // output character in Label 29 output.Text = threadName + ": " + displayChar; 30 } // end method DisplayCharacter 31 32 // place random characters in GUI 33 public void GenerateRandomCharacters() 34 { 35 // get name of executing thread 36 threadName = Thread.CurrentThread.Name; 37 38 while ( true ) // infinite loop; will be terminated from outside 39 { 40 // sleep for up to 1 second 41 Thread.Sleep( generator.Next( 1001 ) ); 42 43 lock ( this ) // obtain lock 44 { 45 while ( suspended ) // loop until not suspended 46 { 47 Monitor.Wait( this ); // suspend thread execution 48 } // end while 49 } // end lock 50 51 // select random uppercase letter 52 char displayChar = ( char ) ( generator.Next( 26 ) + 65 ); 53 54 // display character on corresponding Label 55 output.Invoke( new DisplayDelegate( DisplayCharacter ), 56 new object[] { displayChar } ); 57 } // end while 58 } // end method GenerateRandomCharacters 59 60 // change the suspended/running state 61 public void Toggle() 62 { 63 suspended = !suspended; // toggle bool controlling state 64 65 // change label color on suspend/resume 66 output.BackColor = suspended ? Color.Red : Color.LightGreen; 67 68 lock ( this ) // obtain lock 69 { 70 if ( !suspended ) // if thread resumed 71 Monitor.Pulse( this ); 72 } // end lock 73 } // end method Toggle 74 } // end class RandomLetters |
Figure 15.14. GUIThreads demonstrates multithreading in a GUI application.
(This item is displayed on pages 756 - 757 in the print version)
1 // Fig. 15.14: GUIThreads.cs 2 // Demonstrates using threads in a GUI 3 using System; 4 using System.Windows.Forms; 5 using System.Threading; 6 7 public partial class GUIThreadsForm : Form 8 { 9 public GUIThreadsForm() 10 { 11 InitializeComponent(); 12 } // end constructor 13 14 private RandomLetters letter1; // first RandomLetters object 15 private RandomLetters letter2; // second RandomLetters object 16 private RandomLetters letter3; // third RandomLetters object 17 18 private void GUIThreadsForm_Load( object sender, EventArgs e ) 19 { 20 // create first thread 21 letter1 = new RandomLetters( thread1Label ); 22 Thread firstThread = new Thread( 23 new ThreadStart( letter1.GenerateRandomCharacters ) ); 24 firstThread.Name = "Thread 1"; 25 firstThread.Start(); 26 27 // create second thread 28 letter2 = new RandomLetters( thread2Label ); 29 Thread secondThread = new Thread( 30 new ThreadStart( letter2.GenerateRandomCharacters ) ); 31 secondThread.Name = "Thread 2"; 32 secondThread.Start(); 33 34 // create third thread 35 letter3 = new RandomLetters( thread3Label ); 36 Thread thirdThread = new Thread( 37 new ThreadStart( letter3.GenerateRandomCharacters ) ); 38 thirdThread.Name = "Thread 3" ; 39 thirdThread.Start(); 40 } // end method GUIThreadsForm_Load 41 42 // close all threads associated with this application 43 private void GUIThreadsForm_FormClosing( object sender, 44 FormClosingEventArgs e ) 45 { 46 System.Environment.Exit( System.Environment.ExitCode ); 47 } // end method GUIThreadsForm_FormClosing 48 49 // suspend or resume the corresponding thread 50 private void threadCheckBox_CheckedChanged( object sender, 51 EventArgs e ) 52 { 53 if ( sender == thread1CheckBox ) 54 letter1.Toggle(); 55 else if ( sender == thread2CheckBox ) 56 letter2.Toggle(); 57 else if ( sender == thread3CheckBox ) 58 letter3.Toggle(); 59 } // end method threadCheckBox_CheckedChanged 60 } // end class GUIThreadsForm |
When the thread awakens, line 43 locks this RandomLetters object, so we can determine whether the thread has been suspended (i.e., the user clicked the corresponding CheckBox). Lines 4548 loop while the bool variable suspended remains true. Line 47 calls Monitor method Wait on this RandomLetters object to temporarily release the lock and place this thread into the WaitSleepJoin state. When this thread is Pulsed (i.e., the user clicks the corresponding CheckBox again), it moves back to the Running state. If suspended is false, the thread resumes execution. If suspended is still true, the loop executes again and the thread re-enters the WaitSleepJoin state.
Line 52 generates a random uppercase character. Lines 5556 call method Invoke, passing to it a new DisplayDelegate containing method DisplayCharacter and a new array of objects that contains the randomly generated letter. Line 23 declares a delegate type named DisplayDelegate, which represents methods that take a char argument and do not return a value. Method DisplayCharacter (lines 2630) meets those requirementsit receives a char parameter named displayChar and does not return a value. The call to Invoke in lines 5556 will cause the UI thread to call DisplayCharacter with the randomly generated letter as the argument. At that time, line 29 will replace the text in the Label associated with this RandomLetters object with the name of the Thread executing this RandomLetters object's GenerateRandomCharacters method and the randomly generated letter.
When the user clicks the CheckBox to the right of a particular Label, the corresponding thread should be suspended (temporarily prevented from executing) or resumed (allowed to continue executing). Suspending and resuming a thread can be implemented by using thread synchronization and Monitor methods Wait and Pulse. Lines 6173 declare method Toggle, which will change the suspended/resumed state of the current thread. Line 63 reverses the value of bool variable suspended. Line 66 changes the background color of the Label by assigning a color to Label property BackColor. If the thread is suspended, the background color will be Color.Red. If the thread is running, the background color will be Color.LightGreen. Method Toggle is called from the event handler in Fig. 15.14, so its tasks will be performed in the UI threadthus, there is no need to use Invoke for line 66. Line 68 locks this RandomLetters object, so we can determine whether the thread should resume execution. If so, line 71 calls method Pulse on this RandomLetters object to alert the thread that was placed in the WaitSleepJoin state by the Wait method call in line 47.
Note that the if statement in line 70 does not have an associated else. If this condition fails, it means that the thread has just been suspended. When this happens, a thread executing at line 45 will enter the while loop, and line 47 will suspend the thread with a call to method Wait.
Class GUIThreads (Fig. 15.14) displays three Labels and three CheckBoxes. A separate thread of execution is associated with each Label and CheckBox pair. Each thread randomly displays letters from the alphabet in its corresponding Label object. Lines 21, 28 and 35 create three new RandomLetters objects. Lines 2223, 2930 and 3637 create three new Threads that will execute the RandomLetters objects' GenerateRandomCharacters methods. Lines 24, 31 and 38 assign each Thread a name, and lines 25, 32 and 39 Start the THReads.
If the user clicks the Suspended CheckBox next to a particular Label, event handler threadCheckBox_CheckedChanged (lines 5059) determines which CheckBox generated the event and calls its associated RandomLetters object's Toggle method to suspend or resume the thread.
Lines 4347 define the GUIThreadsForm_FormClosing event handler, which calls method Exit of class System.Environment with the ExitCode property as as argument. This causes all other threads in this application to terminate. Otherwise, only the UI thread would be terminated when the user closes this application; THRead1, THRead2 and Thread3 would continue executing forever.