Thread Synchronization

I l @ ve RuBoard

As stated before, threads can be scheduled to run at any time and in any order. It often is necessary to coordinate various threads and to share resources among threads. One word of initial caution: All multithread applications work in debug mode; it's only in release mode when things start to go haywire. Debugging multithreaded applications takes time and experience; nothing else seems to help make life any easier when dealing with these multi-headed monsters. For some unknown and unexplainable reason, thread scheduling in Debug mode in no way represents the types of scheduling found in Release mode. Mainly, this is a side effect introduced by the debugging environment.

lock Keyword

C# has the lock keyword. lock can be used to synchronize access to both instance and static fields of an object. To synchronize access to instance based fields, an application would use lock( this ) where this is a reference to the current object. To synchronize access to static fields, the lock keyword would be used in conjunction with the typeof keyword as follows :

 lock( typeof( class ) ) 

To illustrate the use of the lock keyword and the possible effects of not using it, Listing 5.4.5 shows a simple example of accessing an instance-based array. Two threads are created and each thread attempts to place its hash code into individual cells within the array. When the lock statement is active, the test program runs without any errors. However, to see the effect of not using the lock keyword, merely comment out the lock statement and run the same example.

Listing 5.4.5 The lock Keyword
 1: using System;  2: using System.Threading;  3:  4:  5: public class ThreadLockTest {  6:  7:     private int[]    m_Array = new int[10];  8:     private int      m_CurrentIndex = 0;  9: 10: 11:     public  void ThreadProc( ) { 12: 13:         int ThreadId = Thread.CurrentThread.GetHashCode( ); 14: 15:         for( int i = 0; i < 10; i++ ) 16:            //comment out the lock statement 17:            //and watch the exceptions fly 18:            lock( this ) { 19:                if( m_CurrentIndex < 10 ) { 20:                    Thread.Sleep( (new Random()).Next( 2 ) * 1000 ); 21:                    m_Array[m_CurrentIndex++] = ThreadId; 22:                } 23:             } //lock block 24:     } 25: 26:     public void PrintArray( ) { 27:         for( int i = 0; i < 10; i++ ) 28:            Console.WriteLine( "m_Array[{ 0} ] = { 1} ", i, m_Array[i] ); 29:     } 30: 31: 32: 33:     public static void Main( ) { 34: 35:         ThreadLockTest tlt = new ThreadLockTest( ); 36:         Thread t1 = new Thread( new ThreadStart( tlt.ThreadProc ) ); 37:         Thread t2 = new Thread( new ThreadStart( tlt.ThreadProc ) ); 38: 39:          t1.Start( ); 40:          t2.Start( ); 41:          t1.Join( ); 42:          t2.Join( ); 43: 44:          tlt.PrintArray( ); 45:      } 46: } 

Again, with the lock statement in place, the test program runs fine and each index of the array will contain the hash code of the given thread. However, if the lock statement is commented out, chances are that an IndexOutOfRangeException exception will be raised. This is due to the test on line 19. After the m_CurrentIndex is checked, the current thread is put to sleep for a random period of time. In that time period, it is possible for the other thread to run and possibly to invalidate the m_CurrentIndex by incrementing the value past 10.

Mutex

Similar to the lock keyword, a Mutex represents mutually exclusive access to one or more resources. Unlike a critical section, a Mutex is a kernel level object and, as such, can be shared across processes. As a side effect, the time required to acquire the Mutex is much longer than that of a critical section using the lock keyword.

Rather than build a small console program to demonstrate the use of a Mutex , a small Windows Forms application will be created. The idea is to show how two different processes can access the same Mutex . For multiple processes to gain access to a shared Mutex , there needs to be some mechanism to acquire the same Mutex . In fact, this is very simple to accomplish and is done by creating a Mutex with a shared name in the form of a formal string parameter. The string parameter then can be used by multiple processes to acquire the kernel level Mutex .

Figure 5.4.1 shows two instances of the WinMutex application. When one process acquires the shared Mutex , a green circle will be drawn in the client area. When the Mutex is released or when waiting to acquire the Mutex , the circle will turn red in color .

Figure 5.4.1. WinMutex application.

graphics/0504fig01.gif

Listing 5.4.6 shows the implementation for WinMutex . Of particular note is the construction of the Mutex object that will be accessed by multiple instances of this application.

Listing 5.4.6 WinMutex Application
 1: using System;   2: using System.Drawing;   3: using System.Collections;   4: using System.ComponentModel;   5: using System.Windows.Forms;   6: using System.Data;   7: using System.Threading;   8:   9: namespace WinMutex  10: {  11:  12:     public class MainForm : System.Windows.Forms.Form  13:     {  14:  15:         private System.ComponentModel.IContainer components;  16:  17:         private Mutex m = new Mutex( false, "WinMutex" );  18:         private bool bHaveMutex = false;  19:  20:         public MainForm() {  21:  22:             InitializeComponent();  23:  24:             //  25:             // TODO: Add any constructor code after InitializeComponent call  26:             //  27:             Thread T = new Thread( new ThreadStart( ThreadProc ) );  28:             T.Start( );  29:  30:           }  31:  32:           /// <summary>  33:           /// Clean up any resources being used.  34:           /// </summary>  35:           public override void Dispose()  36:           {  37:             if (components != null)  38:             {  39:                components.Dispose();  40:             }  41:                base.Dispose();  42:           }  43:  44:           #region Windows Form Designer generated code  45:           /// <summary>  46:           /// Required method for Designer support - do not modify  47:           /// the contents of this method with the code editor.  48:           /// </summary>  49:           private void InitializeComponent()  50:           {  51:               this.components = new System.ComponentModel.Container();  52:               //  53:               // MainForm  54:               //  55:               this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);  56:               this.BackColor = System.Drawing.SystemColors.Window;  57:               this.ClientSize = new System.Drawing.Size(292, 273);  58:               this.Name = "MainForm";  59:               this.Text = "WinMutex";  60:  61:            }  62:          #endregion  63:  64:           protected override void OnPaint( PaintEventArgs e ) {  65:  66:              if( this.bHaveMutex )  67:                DrawCircle( System.Drawing.Color.Green );  68:              else  69:                DrawCircle( System.Drawing.Color.Red );  70:           }  71:  72:           protected void DrawCircle( System.Drawing.Color color ) {  73:  74:              Brush b = new SolidBrush( color );  75:              System.Drawing.Graphics g = this.CreateGraphics( );  76:              int x = this.Size.Width / 2;  77:              int y = this.Size.Height / 2;  78:  79:              g.FillEllipse( b, 0, 0, this.ClientSize.Width, this.ClientSize.Height );  80:  81:              b.Dispose( );  82:              g.Dispose( );  83:  84:            }  85:  86:  87:            protected void ThreadProc( ) {  88:  89:               while( true ) {  90:                  m.WaitOne( );  91:                  bHaveMutex = true;  92:                  Invalidate( );  93:                  Update( );  94:                  Thread.Sleep( 1000 );  95:                  m.ReleaseMutex( );  96:                  bHaveMutex = false;  97:                  Invalidate( );  98:                  Update( );  99:               } 100:             } 101: 102:             /// <summary> 103:             /// The main entry point for the application. 104:             /// </summary> 105:             [STAThread] 106:             static void Main() 107:             { 108:                Application.Run(new MainForm()); 109:             } 110:      } 111: } 

There are a few things to point out about the WinMutex program besides just the use of the shared Mutex . On line 87 of Listing 5.4.6 is the thread procedure ThreadProc . This method is used to acquire the Mutex and to invalidate the parent form so the proper colored circle can be rendered. Why not just call the DrawCircle method from within the ThreadProc method? The answer lies in a little known handle map used by Windows Forms. When attempting to create a graphics context by another thread, an exception will be generated due to a handle map violation. In essence, a duplicate window handle cannot be created, and this is exactly what happens when a child thread attempts to create a graphics context. To avoid this exception, the worker thread merely invalidates the form, which, in turn, generates a WM_PAINT message to be placed in the applications message queue. The main application thread will then process this WM_PAINT message.

Run two instances of WinMutex side by side and you'll notice that the circles are never green at the same time. This is due to the use of the shared Mutex and cross process synchronization achieved by its use.

AutoResetEvent

Another form of cross thread synchronization is that of events. The .NET framework provides both AutoResetEvents and ManualResetEvents . An event is similar to an event in Windows Forms where the application is awaiting some action by the user to generate an event and then responds to that event. In a similar manner, events can be used to synchronize the processing of various threads. The name AutoResetEvent is derived from the fact that the event is reset automatically when a pending thread has been notified of the event.

Consider the following example: One thread is responsible for producing some widget and yet another thread consumes these widgets. This type of problem is known as producer/consumer and is a classic thread synchronization example. In this example, the producer thread will raise an event each time a widget is produced. In response to the raised event, the consumer thread will do something with the newly created widget. Listing 5.4.7 shows the basic use of the AutoResetEvent .

Listing 5.4.7 The AutoResetEvent
 1: using System;  2: using System.Threading;  3:  4: namespace ProducerConsumer {  5:  6:      public class Factory {  7:  8:         private int[]  Widgets = new int[100];  9:         private int    WidgetIndex = 0; 10:         private AutoResetEvent NewWidgetEvent =  new AutoResetEvent( false ); 11: 12: 13:         protected void Producer( ) { 14: 15:              while( true ) {    //run forever 16: 17:                  lock( this ) { 18:                    if( WidgetIndex < 100 ) { 19:                       Widgets[ WidgetIndex ] = 1; 20:                       Console.WriteLine("Widget { 0}  Produced",  WidgetIndex++ ); 21:                       NewWidgetEvent.Set( ); 22:                    } 23:                   } 24: 25:                  Thread.Sleep( (new Random()).Next( 5 ) * 1000 ); 26:              } 27:         } 28: 29: 30:         protected void Consumer( ) { 31: 32:            while( true ) { 33:               NewWidgetEvent.WaitOne( ); 34:               int iWidgetIndex = 0; 35: 36:               lock( this ) { 37:                    iWidgetIndex = --this.WidgetIndex; 38:                    Console.WriteLine("Consuming widget { 0} ", iWidgetIndex ); 39:                    Widgets[ iWidgetIndex-- ] = 0; 40:                  } 41:              } 42:         } 43: 44:         public void Run( ) { 45:             //Create 3 producers 46:            for( int i = 0; i < 3; i++ ) { 47:               Thread producer = new Thread( new ThreadStart( Producer ) ); 48:                producer.Start( ); 49:            } 50:                 //Create 3 consumers 51:             for( int i = 0; i < 3; i++ ) { 52:                Thread consumer = new Thread( new ThreadStart( Consumer ) ); 53:                consumer.Start( ); 54:             } 55: 56:         } 57: 58:         public static void Main( ) { 59:             Factory factory = new Factory( ); 60:             factory.Run( ); 61:         } 62:     } 63: } 

ManualResetEvent

Similar to the AutoResetEvent , the ManualResetEvent also can be used to signal a pending thread regarding some event. The difference is that a ManualResetEvent has to be reset through a method invocation rather than automatically being reset. This is the only difference as far as the mechanics are concerned . The usage difference between an AutoResetEvent and a ManualResetEvent is really dependent on the types of synchronization that are necessary. With a ManualResetEvent , it is possible to monitor the state of the event to determine when the event becomes reset.

Thread Pools

There are many applications that use threads to handle various client requests . Such applications include Web servers, database servers, and application servers. Rather than spawning a new thread each time to fulfill a client request, thread pools allow threads to be reused. By allocating and reusing threads, the overhead involved in constantly creating and destroying threads can be saved. Although the cost of creating a worker thread is generally small, the additive cost of creating and destroying hundreds of threads begins to be a performance issue.

A thread pool will be created under three different circumstances: The first circumstance is the call to QueueUserWorkItem . The timer queue or a registered wait operation queues a callback function. The second circumstance is the number of threads that will be created and managed by the thread pool, which depends on the amount of memory available to the system and the third circumstance is the implementation of the thread pool manager.

QueueUserWorkItem

To demonstrate the use of QueueUserWorkItem , consider an insurance company's need to process insurance claims. Using a thread pool, each claim can be considered a work item for a thread to complete. When a new claim arrives, the application can queue the claim as a work item for the thread pool. The QueueUserWorkItem method is a static method of the ThreadPool class and has the following signature:

 ThreadPool.QueueUserWorkItem( WaitCallback callback, object state ) 

The first argument is the callback delegate for the thread to execute. Unlike the thread delegates used in the past examples, a WaitCallback delegate allows a formal parameter of type object . The object parameter allows information to be sent to the worker thread. This, of course, differs in the respect that, up until now, there has been no information passing to worker threads. Listing 5.4.8 shows the insurance example using a thread pool to process insurance claims.

Listing 5.4.8 Thread Pools and QueueUserWorkItem
 1: using System;  2: using System.Threading;  3:  4:  5: //The InsClaim represents  6: //a work item for a particular  7: //thread  8: public struct InsClaim {  9:     public string ClaimId; 10:     public double Amount; 11: } 12: 13: 14: public class App { 15: 16:     public void ProcessInsuranceClaim( object state ) { 17:         //unbox the InsClaim passed in 18:         InsClaim theClaim = (InsClaim)state; 19: 20:         //Process the claim and sleep to simulate work 21:         Console.WriteLine("Processing Claim { 0}  for ${ 1}  amount", graphics/ccc.gif theClaim.ClaimId, theClaim.Amount ); 22:         Thread.Sleep( 5000 ); 23:         Console.WriteLine("Claim { 0}  processed", theClaim.ClaimId); 24:     } 25: 26: 27:     public static void Main( ) { 28: 29:         App app = new App( ); 30: 31:         //Create 100 insurance claims 32:         Random r = new Random( ); 33:         for( int i = 0; i < 100; i++ ) { 34:            InsClaim claim; 35:            claim.ClaimId = string.Format("INS{ 0} ", i); 36:            claim.Amount = r.NextDouble( ) * 5000; 37:            ThreadPool.QueueUserWorkItem( new WaitCallback( app.ProcessInsuranceClaim graphics/ccc.gif ), claim ); 38:         } 39: 40:         //allow threads in pool to run 41:         Console.ReadLine( ); 42:     } 43: } 

The insurance claim example in Listing 5.4.8 is fairly straightforward. The for loop, used to create 100 insurance claims to be processed, begins on line 33. Each claim then is queued as a work item for a waiting thread within the thread pool. Be sure to notice the interleaving output produced by the sample. As claims are starting to be processed, other claims being processed by other threads are finishing the processing. Thread pools are an effective tool for such background tasks , especially when threads might be spending time waiting for work.

I l @ ve RuBoard


C# and the .NET Framework. The C++ Perspective
C# and the .NET Framework
ISBN: 067232153X
EAN: 2147483647
Year: 2001
Pages: 204

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