16.4. Thread Synchronization

Certain programming instructions are guaranteed to be never preempted by a thread X while another thread Y is running them. Such atomic operations have built-in thread safety. However, in OOP, operations (method calls) are seldom atomic, and the state of the object is rarely as simple as incrementing an integer.

Objects that are designed for thread safety do not allow multiple threads to simultaneously change their state. Such objects identify certain sections of the code as critical and put appropriate locks around that code so that only one thread can call that block of code at any one time. Running the block of code requires the caller thread to acquire a lock that prevents other threads from accessing that code. Upon finishing the block of code, the caller thread relinquishes the lock for other threads to use. The larger the block of code on which the lock is obtained, the longer the lock will be held by the calling thread.

C# provides the following ways to achieve thread synchronization:

  • Using very basic synchronization primitives such as WaitHandles, Mutexes, ReaderWriterLocks, and Interlocked. These constructs can be used for interprocess synchronization.

  • Using SynchronizedAttribute to enable synchronization of ContextBoundObject objects. You can use this technique to synchronize only instance fields and methods. All objects in the same context domain share the same lock.

  • Using the Monitor class. This approach is better appreciated by Java programmers. The Monitor class consists of static methods to which you pass the object you want to synchronize. C# provides a lock keyword. This is similar to Java's synchronize keyword, but unlike synchronize, the lock keyword cannot be used in a method signature. Programmers can choose to use the Monitor.Enter and Monitor.Exit methods or use the lock keyword.

The following two syntaxes achieve equivalent results. Syntax 1:

lock (this) {
}

Syntax 2:

try {
  Monitor.Enter(this);
} finally {
  Monitor.Exit(this);
}

Listing 16.13 shows four threads trying to access an instance field. In this example, at any given time the state of the field val in the BloodBank class is unpredictable because it is modified by four threads without any synchronization. The behavior of Listing 16.13 will be different on different systems.

Listing 16.13. Multithreaded Code without Synchronization (C#)
using System;
using System.Threading;

public class EntryPointClass {
  public static void Main(string[] args) {
    BloodBank bb = new BloodBank();
    Donor d1 = new Donor("D1",1000, bb);
    Donor d2 = new Donor("D2",2000, bb);
    Taker t1 = new Taker("T1",1000, bb);
    Taker t2 = new Taker("T2",2000, bb);
  }
}

class Donor {
  Thread t;
  BloodBank bb;
  string name;
  int sleep;

  public Donor(string name, int sleep, BloodBank bb) {
    this.name = name;
    this.bb = bb;
    this.sleep = sleep;
    t = new Thread(new ThreadStart(Donate));
    t.Start();
  }
  public void Donate() {
    while (true) {
      Thread.Sleep(this.sleep);
      this.bb.add();
    }
  }
}
class Taker {
  Thread t;
  BloodBank bb;
  string name;
  int sleep;

  public Taker(string name, int sleep, BloodBank bb) {
    this.name = name;
    this.bb = bb;
    this.sleep = sleep;
    t = new Thread(new ThreadStart(Take));
    t.Start();
  }
  public void Take() {
    while (true) {
      Thread.Sleep(this.sleep);
      Console.WriteLine("Blood count "+this.bb.remove());
    }
  }
}

class BloodBank {

  int val;
  public void add() {
    ++this.val;
  }
  public int remove() {
    return—this.val;
  }

}

A safer method is to synchronize on the add and remove methods of the BloodBank object. You can do this in C# by providing the following constructs:

public void add() {
  lock (this) {
  ++this.val;
      }
}

public int remove() {
  lock (this) {
    return—this.val;
  }
}

This is equivalent to having synchronized add() and remove() methods in Java.

It is important to note that threads always block each other over synchronized methods. Other threads can continue to access other “unsynchronized” methods of the object while a thread is having a lock on the object. This means that if there are two methods such as the preceding add() and remove(), only one thread can call any one of those two methods at any given time, although multiple threads could call an unsynchronized method on the same object.

It is not necessary to always lock on an object's state. Some objects are designed to be thread-unsafe and leave the onus of thread safety on the usage of the object. Servlets in Java, for example, can be accessed by multiple threads at one time as long as those threads do not share the servlet state with each other. Because servlets traditionally do not have any state associated with them, multiple threads can call the doGet() or doPost() method on them. However, servlets can be made thread-safe by implementing the SingleThreadModel, which automatically creates a servlet instance per request (this will also slow your Web application tremendously).

The wait(), notify(), and notifyAll() methods of Java's java.lang.Object have the following equivalents in C#: Monitor.Wait(object), Monitor.Pulse(object), and Monitor.PulseAll (object). Here, object is the object to wait on (usually the object on which the lock is obtained). Thread interaction, discussed earlier, is conducted using Thread.Sleep(). This is a crude approach to make threads cooperate with each other. Also, it is not always feasible to make threads cooperate with each other by querying their ThreadState, especially when the number of threads to be managed is unknown.

To make threads communicate, C# provides the Monitor.Wait and Monitor.Pulse constructs. Listing 16.14 shows an example of these two as well as the lock keyword.

Listing 16.14. Maintaining WaterQuality Using Monitor.Wait() and Monitor.Pulse (C#)
using System;
using System.Threading;
public class WaterQuality {

  private long PotableLimit;
  private long PollutedLimit;
  private long pollution;

  public WaterQuality(long PotableLimit, long PollutedLimit,
                                       long currentPollution) {
    this.PotableLimit = PotableLimit;
    this.PollutedLimit = PollutedLimit;
    this.pollution = currentPollution;
  }

  public void Purify() {
    lock(this) {
      while (true) {
        while (pollution < PotableLimit) {
          Monitor.Wait(this);
        }
        — —pollution;
        Console.WriteLine("Purifying = "+pollution);
        Monitor.PulseAll(this);
      }
    }
  }

  public void Pollute() {
    lock(this) {
      while (true) {
        while (pollution > = PollutedLimit) {
          Monitor.Wait(this);
        }
        ++pollution;
        Console.WriteLine("Polluting = "+pollution);
        Monitor.PulseAll(this);
      }
    }
  }
  static void Main(string[] args) {
    WaterQuality ct = new WaterQuality (10, 200, 3000);
    Thread polluter = new Thread(new ThreadStart(ct.Pollute));
    Thread purifier = new Thread(new ThreadStart(ct.Purify));
    polluter.Start();
    purifier.Start();

  }

}

The WaterQuality class is subject to pollution and purification through the Pollute() and Purify() methods, which are called by two different threads. We want to maintain the water quality between the potable and the polluted limit. The Pollute() and Purify() methods use the Wait() and PulseAll() methods to make the two threads take turns polluting and purifying. Note a couple of things:

  • The lock keyword locks onto the object instance this. This is similar to using synchronize in Java on the entire method.

  • Monitor.Wait() and Monitor.Pulse must be called within the lock (this) block. This is similar to wait() and notify() in Java, which must be called within synchronized blocks.

The program execution can be explained as follows. Initially, both the polluter and the purifier thread start. The polluter thread obtains a lock on this (the WaterQuality object), thereby preventing the Purifier thread from doing anything. At that point the Purifier thread is in the Running state but is not doing any purification. The Polluter thread blocks on the Monitor.Wait(this) call because the condition in the while loop of the Pollute method is true (the initial pollution, 3,000, is greater than 200). The Polluter thread relinquishes the lock and enters the object's wait queue (a queue of threads waiting to obtain the lock). The Purifier thread is notified of the lock availability through the Monitor.PulseAll(this) method. The Purifier exits the wait queue and then enters the ready queue and takes over, purifying the water all the way to the potable limit (10), after which it does not purify any more. At this point, it blocks on the WaterQuality object (through the Monitor.Wait(this) call in the Purify method because the condition in the while loop of the Purify method is true).

The Purifier thread then relinquishes the previously obtained lock from the Polluter and notifies the Polluter to take over. The Polluter takes over again, now incrementing the pollution up to 200, after which it blocks again, asking the purifier to start purifying. This cycle keeps repeating, and the two threads work endlessly to keep the WaterQuality pollution between 10 and 200.

You can change the interaction between the threads by changing the values for the potable limit, the pollution limit, and the current pollution. A current pollution of less than 10 will trigger the Polluter to work first and bump the pollution to 200.

Had we not used the Monitor.Wait and the Monitor.Pulse methods in this example, the Polluter thread would have cannibalized CPU time because it would never have relinquished the lock on the WaterQuality object while it was busy polluting the water. The Monitor.Wait(this) method tells the calling thread to wait on this. The calling thread then goes into a wait queue and stays in the wait queue until it is told to move to the ready queue through the Monitor.Pulse(this) call. The Monitor.Pulse(this) notifies the thread waiting in the wait queue for this object's lock. Note that you are encouraged to use Monitor.PulseAll(this) instead of Monitor.Pulse().

Locking can be done on any object. Typically, you lock the object that is going to be changed or accessed by multiple threads. You can create a static lock by locking on a static object. This is equivalent to synchronizing on a static object in Java.

16.4.1. The ReaderWriterLock Class

Earlier we found that in C#, synchronizing a block of code is as easy as wrapping a lock construct around the code. The most thread-safe object would have all its methods synchronized. Synchronizing all the methods would also slow the application, however, because only one thread could work on the object at any given time. There are several situations when you want multiple threads read from the object but have only one thread modify the object. Let's say we are modeling a database table as shown in Listing 16.15.

Listing 16.15. Database Model (C#)
public class Table {

  public Row Read() {
    lock (this) {
    //Read
    }
  }
  public void Write(Row) {
    lock (this) {
    //Write
    }
  }
}

If we had 100 threads trying to access the Table object, only one thread could actually either read or write to the table at a time. What we want is a mechanism for multiple threads to be able to read from the table and only a single thread to be able to write to it. The C# ReaderWriterLock class can be used for classes that allow multiple reads and a single write; this means that the state of the object can be read by multiple threads as long as there is no thread trying to mutate the state of the object.

Listing 16.16 has been borrowed from the sample programs that ship with the .NET SDK. The program uses a thread pool to simulate 20 threads trying to read and write to a table. The odd-numbered threads read from the table, and the even-numbered threads write to the table. Note the use of AutoResetEvent to signal execution completion of the ThreadPool thread. A new class, Interlocked, is used to decrement the number of threads that have accessed the table object. We discuss the Interlocked class in Section 16.5.

Listing 16.16. ReaderWriter Lock Demonstration
using System;
using System.Threading;

public class Table {
  ReaderWriterLock rwl = new ReaderWriterLock();

  public void Read(int threadNum) {
    rwl.AcquireReaderLock(Timeout.Infinite);
    try {
      Console.WriteLine("Start Resource reading
                               (Thread = {0})", threadNum);
      Thread.Sleep(1000);
       Console.WriteLine("Stop Resource reading
                               (Thread = {0})", threadNum);
    }
    finally {
       rwl.ReleaseReaderLock();
    }
  }

  public void Write(Int32 threadNum) {
    rwl.AcquireWriterLock(Timeout.Infinite);
    try {
      Console.WriteLine("Start Resource
          writing (Thread = {0})", threadNum);
      Thread.Sleep(750);
      Console.WriteLine("Stop Resource writing (Thread =
{0})", threadNum);
    }
    finally {
       rwl.ReleaseWriterLock();
    }
  }
}

public class Test {

  static Int32 numThreads = 20;
  static AutoResetEvent done = new AutoResetEvent(false);
  static Table table;

  static void Main(string[] args) {
      table = new Table();
      for (Int32 i = 0; i < 20; i++) {
        ThreadPool.QueueUserWorkItem(
              new WaitCallback(UpdateResource), i);
      }
      done.WaitOne();
  }

  static void UpdateResource(Object state) {
    Int32 threadNum = (Int32) state;
    if ((threadNum % 2) ! = 0) table.Read(threadNum);
    else table.Write(threadNum);
    if (Interlocked.Decrement(ref numThreads)==0)
         done.Set();
  }
}

The output of Listing 16.16 is as follows:

Start Resource writing (Thread=0)
Stop Resource writing (Thread=0)
Start Resource reading (Thread=1)
Stop Resource reading (Thread=1)
Start Resource writing (Thread=2)
Stop Resource writing (Thread=2)
Start Resource reading (Thread=7)
							Start Resource reading (Thread=3)
							Start Resource reading (Thread=5)
							Stop Resource reading (Thread=7)
							Stop Resource reading (Thread=3)
							Stop Resource reading (Thread=5)
Start Resource writing (Thread=4)
Stop Resource writing (Thread=4)
Start Resource reading (Thread=15)
							Start Resource reading (Thread=9)
							Start Resource reading (Thread=11)
							Start Resource reading (Thread=13)
							Stop Resource reading (Thread=15)
							Stop Resource reading (Thread=9)
							Stop Resource reading (Thread=11)
							Stop Resource reading (Thread=13)
Start Resource writing (Thread=6)
Stop Resource writing (Thread=6)
Start Resource reading (Thread=17)
							Start Resource reading (Thread=19)
							Stop Resource reading (Thread=17)
							Stop Resource reading (Thread=19)
Start Resource writing (Thread=8)
Stop Resource writing (Thread=8)
Start Resource writing (Thread=10)
Stop Resource writing (Thread=10)
Start Resource writing (Thread=12)
Stop Resource writing (Thread=12)
Start Resource writing (Thread=14)
Stop Resource writing (Thread=14)
Start Resource writing (Thread=16)
Stop Resource writing (Thread=16)
Start Resource writing (Thread=18)
Stop Resource writing (Thread=18)

The blocks that have contiguous reads (multiple threads reading at the same time) are shown in boldface. Note that multiple threads can read, but only one thread can write at a time.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.80.3