17.3 Thread synchronization


Of course multithreading comes with a whole host of synchronization problems. There are concurrency problems which lead to indefinite states, [8] and other complicated 'programming traps' such as race conditions and deadlocks. Hence, it is important for a C# developer using threads to be well versed in thread synchronization.

[8] For example, if we have two concurrently running threads getting and setting the value of a shared variable, the exact value stored in this variable will be indefinite at any one time.

In this section, I will introduce the lock keyword, and two more classes “ Mutex and Monitor .

Table 17.3 gives some initial clues as to how thread synchronization compares in C# and Java.

17.3.1 Thread safety in .NET classes

According to documentation from Microsoft, all public and static methods , properties, or fields of the .NET BCL support concurrent access within a multi-threaded environment. This means that any such member can be invoked simultaneously from multiple threads without problems of deadlocks, race conditions, or other synchronization problems. However, not all classes and structs of the BCL are thread safe. [9] If you intend to use non-thread safe classes within a multi-threaded context, and there is a possibility of synchronization problems with these classes, you can wrap the class instance within your own synchronization code.

[9] When you say that a class is 'thread safe', it means that member of an instance of this class will always maintain a valid state when used simultaneously by concurrent threads. You will have to refer to the API documentation of each class under the Thread Safety section to determine if that class is thread safe or not. If all classes are made thread safe, you are going to get lousy performance indeed.

Table 17.3. Corresponding thread synchronization methods of Java and C#

C#

Java

lock keyword or Monitor.Enter() / Monitor.Exit()

synchronized keyword

Monitor.Wait()

Object.wait()

Monitor.Pulse()/Monitor.PulseAll()

Object.notify()/Object.notifyAll()

17.3.2 Using the lock keyword

The C# equivalent of Java's synchronize keyword, is lock . However, unlike the synchronize keyword, which can be used to declare a synchronized block or method, C#'s lock can only be used to declare a synchronized block. You cannot use the lock keyword in a method declaration.

lock takes in an object which acts as the mutex. [10] When a thread attempts to enter the critical section, [11] it checks to see if the mutex object is available. If so, it obtains the mutex and enters the critical section “ only to release it after it leaves the critical section. Another thread attempting to enter the critical section can only do so if no other thread is holding onto the mutex. If the mutex has been taken, the second thread will be blocked until the mutex is released. Here's the syntax.

[10] Mutex stands for 'mutual exclusion'. A mutex is a token used as a flag to determine if someone is already holding on to a particular resource (in our case, the resource is a block of code). If you have heard of the term 'semaphore', this is similar to a mutex, except that a semaphore maintains a counter so that n numbers of threads or processes can access a resource concurrently, and the n +1 th request will be rejected. (By the way, all this discussion reminds me of the 'operating system' module I did in my undergraduate days.)

[11] The critical section is the group of statements delimited by the curly brackets of a lock statement.

 lock (<expression>){   // critical section code } 

<expression> is the mutex object for this critical section, and must be a reference type. Typically, expression is either:

  • an object reference variable or

  • this or

  • typeof ( <class_name> ).

The last expression is often used to protect a shared static variable from concurrent access.

Here is a slightly modified version of the first threading example (section 17.2.2). It has been modified to include a random sleep period for each thread, so that the results can be more easily observed . [12]

[12] Without the random sleep, chances are that each allocated time slice of my operating system is long enough for either thread to perform the whole for loop all at one shot. And the output will either be all A s followed by all B s, or vice versa.

 1: using System;  2: using System.Threading;  3:  4: public class TestClass{  5:  6:   public static void PrintA(){  7:     for (int i=0; i<10; i++){  8:  Random r  =  new Random();  9:  Thread.Sleep(r.Next() % 2000);  10:       Console.Write("A"); 11:     } 12:   } 13: 14:   public static void PrintB(){ 15:     for (int i=0; i<10; i++){ 16:  Random r  =  new Random();  17:  Thread.Sleep(r.Next() % 1000);  18:       Console.Write("B"); 19:     } 20:   } 21: 22:   public static void Main(){ 23:     ThreadStart ts1 = new ThreadStart(PrintA); 24:     ThreadStart ts2 = new ThreadStart(PrintB); 25: 26:     Thread t1 = new Thread(ts1); 27:     Thread t2 = new Thread(ts2); 28: 29:     t1.Start(); 30:     t2.Start(); 31:   } 32: } 

Output:

 c:\expt>test ABBBBAABBABBABBAAAAA 

Lines 8 “ 9 and 16 “ 17 are the random pauses introduced in both methods. Instead of an inconsequential Console.Write statement (on lines 10 and 18), you could have critical code there which involves the reading or updating of a static shared variable. If that is the case, you can lock both methods on a common mutex object so that only one of the two can execute concurrently.

In the fragment below, methods PrintA() and PrintB() have been altered to include the lock keyword demarcating the simulated critical section.

 6:   public static void PrintA(){  7:  lock(typeof(TestClass)){  8:       for (int i=0; i<10; i++){  9:         Random r = new Random(); 10:         Thread.Sleep(r.Next() % 2000); 11:         Console.Write("A"); 12:       } 13:  }  14:   } 15: 16:   public static void PrintB(){ 17:  lock(typeof(TestClass)){  18:       for (int i=0; i<10; i++){ 19:         Random r = new Random(); 20:         Thread.Sleep(r.Next() % 1000); 21:         Console.Write("B"); 22:       } 23:  }  24:   } 

Output:

 c:\expt>test BBBBBBBBBBAAAAAAAAAA c:\expt>test AAAAAAAAAABBBBBBBBBB 

I have used the typeof (TestClass) as the mutex object. [13] If you are dealing with non-static methods, you can use a reference to any common object accessible by both methods as the mutex. ( this is very commonly used if both threads are running on the same object instance.) Depending on which thread obtains the mutex first, either all the B s are printed out before an A appears, or vice versa. Both possible scenarios are shown in the output above.

[13] typeof is a C# keyword which returns a System.Type object.

17.3.3 Using Monitor

System.Threading.Monitor is a sealed [14] class which contains several useful static methods for locking operations. The most commonly used methods of Monitor are Monitor.Enter() and Monitor.Exit() . Both take in a mutex object reference:

[14] A sealed class in C# is a final class in Java.

 public static void Enter (object obj); public static void Exit (object obj); 

These two methods do exactly the same thing as the lock keyword. Assuming the mutex object in use is some reference type called lockingObj , the following two code fragments (lines 30 “ 32 and lines 40 “ 42) are thus functionally identical:

 30      lock (lockingObj){ 31        // critical section codes 32      } 40      Monitor.Enter(lockingObj); 41        // critical section codes 42      Monitor.Exit(lockingObj); 

Probably one advantage of using these two Monitor methods over lock is that you can conditionally execute the Monitor.Exit() method within the critical section. Another advantage is that you can have the Monitor.Enter() statement in one method, and Monitor.Exit() coded in another method, thus having a critical section spanning multiple methods. [15]

[15] This practice, although possible, is highly discouraged because it leads to unstructured code which is difficult to debug.

The Monitor class has other useful static methods which you can use “ Pulse , PulseAll , Wait , and TryEnter .

TryEnter is similar to Enter , except that TryEnter is overloaded to specify a timeout. If some other thread is already hogging the mutex object, using Enter will block the calling thread indefinitely. Using TryEnter , on the other hand, can cause the statement to return on timeout if the calling thread is still unable to obtain the mutex object after the specified time.

TryEnter 's method signatures look like this:

 public static bool TryEnter (object obj); public static bool TryEnter (object obj, int millisecondTimeout); 

For the first overloaded TryEnter , the timeout is defaulted to 0. This means that the method returns immediately if it could not get the mutex object. Both methods return true if the mutex object has been successfully acquired , and false if the acquisition has failed before the timeout expired . Java does not have this built-in mechanism.

Monitor.Pulse() and Monitor.Wait() are similar in functionality to Java's java.lang.Object 's notify() and wait() . Both Wait() and Pulse() should be called only within a critical section (delimited by the lock curly brackets, or in between the Monitor.Enter() and Monitor.Exit() statements.)

Here are the method signatures for Wait() :

 public static bool Wait (object obj); public static bool Wait (object obj, int millisecondTimeout); 

Invoking the first overloaded Wait() releases the mutex object currently being held [16] by the thread, and blocks itself indefinitely until it reacquires the mutex object. When Wait() is invoked, other threads waiting for this mutex object can acquire it and enter the critical section. The method returns true if the thread manages to reacquire the mutex object. It does not return until it has done so. [17]

[16] A thread which is invoking this wait method should already be in the critical section and hence must be holding on to the mutex object.

[17] Meaning that false will never be returned, and making the boolean return value superfluous.

The second overloaded Wait() enables you to specify the timeout in milliseconds . The method returns true if the thread manages to reacquire the mutex object within the specified time. If the calling thread does not manage to reacquire the mutex object before timeout expiry, the method will return false .

Pulse() works together with Wait() . It is invoked by the current thread to notify the next thread waiting for the particular mutex object that there might be a change in the mutex object's state. Essentially, Pulse() tells the waiting thread to ' please go and take a look at the mutex object, and get ready to acquire it if it is available'.

Here is the method signature for Pulse() :

 public static void Pulse (object obj); 

If you intend to notify all threads waiting on the mutex object, use the PulseAll() method instead:

 public static void PulseAll (object obj); 

Before concluding this section with a coded example, I have to mention that each object which is acting as a mutex keeps three references:

  • reference to the thread that has currently acquired it;

  • reference to a waiting queue “ the waiting queue contains a list of threads waiting to be notified about the mutex object's change in state;

  • reference to a ready queue “ The ready queue contains a list of threads ready to acquire the mutex object once it is ready.

A Wait() invocation will place the thread currently holding the mutex object into the waiting queue, so that a thread from the ready queue can acquire it. A Pulse() invocation will cause a thread from the waiting queue to move over to the ready queue. PulseAll() causes a mass migration of all threads in the waiting queue to the ready queue.

Here is a construed example to demonstrate how a mutex object is released via Wait() .

 1: using System;  2: using System.Threading;  3:  4: public class TestClass{  5:  6:   static object mutex = new object();  7:  8:   public static void PrintA(){  9:  Monitor.Enter(mutex);  10:       for (int i=0; i<5; i++){ 11:         Console.WriteLine("A:"+i); 12:         if (i==2){ 13:  Monitor.Pulse(mutex);  14:  Monitor.Wait(mutex);  15:         } 16:       } 17:  Monitor.Exit(mutex);  18:   } 19: 20:   public static void PrintB(){ 21:  Monitor.Enter(mutex);  22:       for (int i=0; i<5; i++) 23:         Console.WriteLine("B:"+i); 24:  Monitor.Pulse(mutex);  25:  Monitor.Exit(mutex);  26:  } 27: 28:  public static void Main(){ 29:    ThreadStart ts1 = new ThreadStart(PrintA); 30:    ThreadStart ts2 = new ThreadStart(PrintB); 31: 32:    Thread t1 = new Thread(ts1); 33:    Thread t2 = new Thread(ts2); 34: 35:    t1.Priority = ThreadPriority.Highest; 36:    t1.Start(); 37:    t2.Start(); 38:  } 39: } 

Output:

 c:\expt>test A:0 A:1 A:2 B:0 B:1 B:2 B:3 B:4 A:3 A:4 

This code demonstrates two threads which are running two separate critical sections (one in PrintA , and the other in PrintB ) both synchronized on the same static object. [18] The first thread which got the mutex object calls a Wait halfway (line 14), so that the second thread can obtain the mutex and execute the critical section.

[18] Of course, you can have multiple threads attempting to enter the same critical section of the same method of the same object too.

An object to be used as the mutex is declared in line 6. Both the code in PrintA and PrintB are synchronized on the same static mutex object. In order to ensure that thread t1 starts first, I have set its priority so that it is higher than t2 on line 35. When t1 starts, it grabs hold of the mutex object on line 9, thus preventing t2 from going beyond line 21 in PrintB when it starts. Under normal circumstances, t2 has to wait for t1 to finish and release the mutex object before it can execute line 22. However, after PrintA has performed three iterations of the for loop (and printed out A:0 , A:1 and A:2 to the console), the condition on line 12 becomes true and the thread invokes Wait (line 14). When that happens, t1 surrenders the mutex object and is itself blocked while t2 grabs hold of the mutex and completes its looping. After t2 is done, it notifies t1 that the mutex is about to be released (line 24), and releases it (line 25). t1 then takes off from where it left off previously and completes its for loop.

17.3.4 Using Mutex

The last [19] of the threading classes I am going to cover is System.Threading. Mutex . Mutex is a class which represents well, a mutex. You can create an instance of Mutex and use it in place of any other object as the mutex lock.

[19] There are other useful classes in the System.Threading namespace, such as Interlocked and ThreadPool . Most of these classes provide useful methods which help to make threading and synchronization more convenient .

Mutex has 2 useful overloaded constructors:

 public Mutex (); public Mutex (bool initiallyOwned); 

The first constructor creates a Mutex object which is owned by the thread which is currently running (and creating that instance of the Mutex ). The second constructor creates a Mutex object with the option of whether the currently running thread shall own it. Passing in a false as parameter will create a Mutex object which is currently not owned by any thread yet.

Mutex has two useful methods:

 public virtual bool WaitOne(); public virtual void Close(); 

Calling WaitOne() of the Mutex object will cause the calling thread to wait indefinitely until the Mutex object is available (i.e. not owned by any other thread). If the Mutex object is available, the calling thread becomes the new owner of the Mutex object, and WaitOne() returns true . The thread owning the Mutex object relinquishes it by calling the Close() method. WaitOne() is also overloaded to take in a timeout period so that, on expiry, the method will return regardless of whether the calling thread has successfully obtained the Mutex object.

Here is an example of how Mutex can be used:

 1: using System;  2: using System.Threading;  3:  4: public class TestClass{  5:  6:  static Mutex m  =  new Mutex (false);  7:  8:   static void PrintString (string toPrint){  9:  m.WaitOne();  10:     for (int i=0; i<10; i++) 11:       Console.Write(toPrint); 12:  m.Close();  13:   } 14: 15:   static void PrintA(){ 16:     PrintString("A"); 17:   } 18: 19:   static void PrintB(){ 20:     PrintString("B"); 21:   } 22: 23:   public static void Main(){ 24:     ThreadStart ts1 = new ThreadStart(PrintA); 25:     ThreadStart ts2 = new ThreadStart(PrintB); 26: 27:     Thread t1 = new Thread(ts1); 28:     Thread t2 = new Thread(ts2); 29: 30:     t1.Start(); 31:     t2.Start(); 32:   } 33: } 

Output:

 c:\expt>test AAAAAAAAAABBBBBBBBBB c:\expt>test BBBBBBBBBBAAAAAAAAAA 

Using the Mutex class is one way of synchronizing code without using Monitor , or the lock keyword.



From Java to C#. A Developers Guide
From Java to C#: A Developers Guide
ISBN: 0321136225
EAN: 2147483647
Year: 2003
Pages: 221
Authors: Heng Ngee Mok

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