Synchronization


When using multiple threads, you will sometimes need to coordinate the activities of two or more of the threads. The process by which this is achieved is called synchronization. The most common reason for using synchronization is when two or more threads need access to a shared resource that can be used by only one thread at a time. For example, when one thread is writing to a file, a second thread must be prevented from doing so at the same time. Another situation in which synchronization is needed is when one thread is waiting for an event that is caused by another thread. In this case, there must be some means by which the first thread is held in a suspended state until the event has occurred. Then the waiting thread must resume execution.

Key to synchronization is the concept of a lock, which controls access to a block of code within an object. When an object is locked by one thread, no other thread can gain access to the locked block of code. When the thread releases the lock, the code block is available for use by another thread.

The lock feature is built into the C# language. Thus, all objects can be synchronized. Synchronization is supported by the keyword lock. Since synchronization was designed into C# from the start, it is much easier to use than you might expect. In fact, for many programs, the synchronization of objects is almost transparent.

The general form of lock is shown here:

 lock(object)  {      / / statements to be synchronized }

Here, object is a reference to the object being synchronized. If you want to synchronize only a single statement, the curly braces are not needed. A lock statement ensures that the section of code protected by the lock for the given object can be used only by the thread that obtains the lock. All other threads are blocked until the lock is removed. The lock is released when the block is exited.

The following program demonstrates synchronization by controlling access to a method called sumIt( ), which sums the elements of an integer array:

 // Use lock to synchronize access to an object. using System; using System.Threading; class SumArray {   int sum;   public int sumIt(int[] nums) {     lock(this) { // lock the entire method       sum = 0; // reset sum       for(int i=0; i < nums.Length; i++) {         sum += nums[i];         Console.WriteLine("Running total for " +                Thread.CurrentThread.Name +                " is " + sum);         Thread.Sleep(10); // allow task-switch       }       return sum;     }   } } class MyThread {   public Thread thrd;   int[] a;   int answer;   /* Create one SumArray object for all      instances of MyThread. */   static SumArray sa = new SumArray();   // Construct a new thread.   public MyThread(string name, int[] nums) {     a = nums;     thrd = new Thread(this.run);     thrd.Name = name;     thrd.Start(); // start the thread   }   // Begin execution of new thread.   void run() {     Console.WriteLine(thrd.Name + " starting.");     answer = sa.sumIt(a);     Console.WriteLine("Sum for " + thrd.Name +                        " is " + answer);     Console.WriteLine(thrd.Name + " terminating.");   } } class Sync {   public static void Main() {     int[] a = {1, 2, 3, 4, 5};     MyThread mt1 = new MyThread("Child #1", a);     MyThread mt2 = new MyThread("Child #2", a);     mt1.thrd.Join();     mt2.thrd.Join();   } }

The output from the program is shown here:

 Child #1 starting. Running total for Child #1 is 1 Child #2 starting. Running total for Child #1 is 3 Running total for Child #1 is 6 Running total for Child #1 is 10 Running total for Child #1 is 15 Running total for Child #2 is 1 Sum for Child #1 is 15 Child #1 terminating. Running total for Child #2 is 3 Running total for Child #2 is 6 Running total for Child #2 is 10 Running total for Child #2 is 15 Sum for Child #2 is 15 Child #2 terminating.

As the output shows, both threads compute the proper sum of 15.

Let’s examine this program in detail. The program creates three classes. The first is SumArray. It defines the method sumIt( ), which sums an integer array. The second class is MyThread, which uses a static object called sa that is of type SumArray. Thus, only one object of SumArray is shared by all objects of type MyThread. This object is used to obtain the sum of an integer array. Notice that SumArray stores the running total in a field called sum. Thus, if two threads use sumIt( ) concurrently, both will be attempting to use sum to hold the running total. Since this will cause errors, access to sumIt( ) must be synchronized. Finally, the class Sync creates two threads and has them compute the sum of an integer array.

Inside sumIt( ), the lock statement prevents simultaneous use of the method by different threads. Notice that lock uses this as the object being synchronized. This is the way lock is normally called when the invoking object is being locked. Sleep( ) is called to purposely allow a task-switch to occur, if one can—but it can’t in this case. Because the code within sumIt( ) is locked, it can be used by only one thread at a time. Thus, when the second child thread begins execution, it does not enter sumIt( ) until after the first child thread is done with it. This ensures that the correct result is produced.

To understand the effects of lock fully, try removing it from the body of sumIt( ). After doing this, sumIt( ) is no longer synchronized, and any number of threads can use it concurrently on the same object. The problem with this is that the running total is stored in sum, which will be changed by each thread that calls sumIt( ). Thus, when two threads call sumIt( ) at the same time on the same object, incorrect results are produced because sum reflects the summation of both threads, mixed together. For example, here is sample output from the program after lock has been removed from sumIt( ):

 Child #1 starting. Running total for Child #1 is 1 Child #2 starting. Running total for Child #2 is 1 Running total for Child #1 is 3 Running total for Child #2 is 5 Running total for Child #1 is 8 Running total for Child #2 is 11 Running total for Child #1 is 15 Running total for Child #2 is 19 Running total for Child #1 is 24 Running total for Child #2 is 29 Sum for Child #1 is 29 Child #1 terminating. Sum for Child #2 is 29 Child #2 terminating.

As the output shows, both child threads are using sumIt( ) at the same time on the same object, and the value of sum is corrupted.

The effects of lock are summarized here:

  1. For any given object, once a lock has been placed on a section of code, the object is locked and no other thread can acquire the lock.

  2. Other threads trying to acquire the lock on the same object will enter a wait state until the code is unlocked.

  3. When a thread leaves the locked block, the object is unlocked.

One other thing to understand about lock is that it should not be used on public types or public object instances. Otherwise, some thread external to your program could alter the state of the lock in ways not understood by your program. Thus, a construct such as lock(this) should not be used on systemwide objects.

An Alternative Approach

Although locking a method’s code, as shown in the previous example, is an easy and effective means of achieving synchronization, it will not work in all cases. For example, you might want to synchronize access to a method of a class you did not create, which is itself not synchronized. This can occur if you want to use a class that was written by a third party and for which you do not have access to the source code. Thus, it is not possible for you to add a lock statement to the appropriate method within the class. How can access to an object of this class be synchronized? Fortunately, the solution to this problem is simple: lock access to the object from code outside the object by specifying the object in a lock statement. For example, here is alternative implementation of the preceding program. Notice that the code within sumIt( ) is no longer locked. Instead, calls to sumIt( ) are locked within MyThread.

 // Another way to use lock to synchronize access to an object. using System; using System.Threading; class SumArray {   int sum;   public int sumIt(int[] nums) {     sum = 0; // reset sum     for(int i=0; i < nums.Length; i++) {       sum += nums[i];       Console.WriteLine("Running total for " +              Thread.CurrentThread.Name +              " is " + sum);       Thread.Sleep(10); // allow task-switch     }     return sum;   } } class MyThread {   public Thread thrd;   int[] a;   int answer;   /* Create one SumArray object for all      instances of MyThread. */   static SumArray sa = new SumArray();   // Construct a new thread.   public MyThread(string name, int[] nums) {     a = nums;     thrd = new Thread(this.run);     thrd.Name = name;     thrd.Start(); // start the thread   }   // Begin execution of new thread.   void run() {     Console.WriteLine(thrd.Name + " starting.");     // Lock calls to sumIt().     lock(sa) answer = sa.sumIt(a);     Console.WriteLine("Sum for " + thrd.Name +                        " is " + answer);     Console.WriteLine(thrd.Name + " terminating.");   } } class Sync {   public static void Main() {     int[] a = {1, 2, 3, 4, 5};     MyThread mt1 = new MyThread("Child #1", a);     MyThread mt2 = new MyThread("Child #2", a);     mt1.thrd.Join();     mt2.thrd.Join();   } }

Here, the call to sa.sumIt( ) is locked, rather than the code inside sumIt( ), itself. The code that accomplishes this is shown here:

 // Lock calls to sumIt(). lock(sa) answer = sa.sumIt(a);

The program produces the same correct results as the original approach.

Locking a Static Method

The following form of lock shows an easy way to lock a static method:

 lock(typeof(class)) {     // locked block }

Here, class is the name of the class that contains the static method. Keep in mind that this approach should not be used on a public type because code external to your program might lock the specified class and prevent its use by other code.

The Monitor Class and lock

The C# keyword lock is really just shorthand for using the synchronization features defined by the Monitor class, which is defined in the System.Threading namespace. Monitor defines several methods that control or manage synchronization. For example, to obtain a lock on an object, call Enter( ). To release a lock, call Exit( ). These methods are shown here:

 public static void Enter(object syncOb) public static void Exit(object syncOb)

Here, syncOb is the object being synchronized. If the object is not available when Enter( ) is called, the calling thread will wait until it becomes available. You will seldom use Enter( ) or Exit( ), however, because a lock block automatically provides the equivalent. For this reason, lock is the preferred method of obtaining a lock on an object when programming in C#.

One method in Monitor that you may find useful on occasion is TryEnter( ). One of its forms is shown here:

 public static bool TryEnter(object syncOb)

It returns true if the calling thread obtains a lock on syncOb and false if it doesn’t. In no case does the calling thread wait. You could use this method to implement an alternative if the desired object is unavailable.

Monitor also defines these three methods: Wait( ), Pulse( ), and PulseAll( ). They are described in the next section.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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