Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Practical .NET2 and C#2: Chapter 5

0.00/5 (No votes)
16 Mar 2006 1  
This chapter describes the System.Threading.Monitor class and the lock C# keyword.

Title Practical .NET2 and C#2
Author Patrick Smacchia
Publisher ParadoxalPress
Published January 2006
ISBN 0-9766132-2-0
Price USD 59.95 (ParadoxalPress price: USD 33.99)
Pages 896
Website www.PracticalDOT.NET (Browse and download the 647 listings and sample chapters)

The following article is an excerpt from chapter 5 of the book Practical .NET2 and C#2.

Contents

Introduction

The System.Threading.Monitor class allows you to make almost any portion of your code executable by only one thread at a time. We call such a block of code a critical section. In further articles extracted from Practical .NET2 and C#2, we'll introduce other means to synchronize access to resources, such as Win32 synchronization objects, the System.Threading.ReaderWriterLock class, the System.Runtime.Remoting.Contexts.SynchronizationAttribute attribute, volatile fields, and the System.Threading.Interlocked class.

The Enter() and Exit() methods

The Monitor class offers the Enter(object) and Exit(object) static methods. These methods take a reference to an object as a parameter. This object constitutes a simple means to uniquely identify the resource which needs to be accessed in a synchronized way. When a thread calls the Enter() method, it waits to have the exclusive access right to the referenced object (it only waits if another thread already has this right). Once this right has been acquired and consumed, the thread releases this right by calling the Exit() on this same object. Notice that using these methods is not that simple, here are some caveats:

  • A thread can call Enter() several times on the same object as long as it calls Exit() as many times on the same object to release its exclusive access rights.
  • A thread can also own exclusive rights on several objects at the same time, but this can lead to a deadlock situation.
  • Don't call the Enter() and Exit() methods on an instance of a value type such as an integer!
  • Do call the Exit() in a finally clause in order to free all exclusive access rights no matter what happens.

The following example illustrate how to use these two methods to protect a static field named counter from concurrent access:

Example1.cs

using System.Threading;
class Program {
   static long counter = 1;
   static void Main() {
      Thread t1 = new Thread( f1 );
      Thread t2 = new Thread( f2 );
      t1.Start(); t2.Start(); t1.Join(); t2.Join();
   }
   static void f1() {
      for (int i = 0; i < 5; i++){
         Monitor.Enter( typeof( Program ) );
         try{
            counter *= counter;
         }
         finally{ Monitor.Exit( typeof( Program ) ); }
         System.Console.WriteLine("counter^2 {0}", counter);
         Thread.Sleep(10);
      }
   }
   static void f2() {
      for (int i = 0; i < 5; i++){
         Monitor.Enter( typeof( Program ) );
         try{
            counter *= 2;
         }
         finally{ Monitor.Exit( typeof( Program ) ); }
         System.Console.WriteLine("counter*2 {0}", counter);
         Thread.Sleep(10);
      }
   }
}

It is tempting to write counter instead of typeof(Program) but counter is a static member of a value type. We�ll see a bit later that there is a better solution which avoids calling the expensive typeof(Program).

Notice that the square and doubling operations are not commutative and thus the final value of counter will not be deterministic.

The lock C# keyword

The C# language presents the lock keyword which is an elegant alternative to using the Enter() and Exit() methods. Our program could then be rewritten like this:

Example2.cs

using System.Threading;
class Program {
   static long counter = 1;
   static void Main() {
      Thread t1 = new Thread(f1);
      Thread t2 = new Thread(f2);
      t1.Start(); t2.Start(); t1.Join(); t2.Join();
   }
   static void f1() {
      for (int i = 0; i < 5; i++){
         lock( typeof(Program) ) { counter *= counter; }
         System.Console.WriteLine("counter^2 {0}", counter);
         Thread.Sleep(10);
      }
   }
   static void f2() {
      for (int i = 0; i < 5; i++){
         lock( typeof(Program) ) { counter *= 2; }
         System.Console.WriteLine("counter*2 {0}", counter);
         Thread.Sleep(10);
      }
   }
}

As with for and if, the blocks defined by the lock keyword are not required to use curly braces if they only contain a single instruction. We could have written:

...
lock( typeof( Program ) ) 
   counter *= counter;
...

The use of the lock keyword provokes the creation by the C# compiler of a try/finally block which allows you to anticipate any exception which might be raised. You can verify this by using the Reflector or the ildasm.exe tool.

The SyncRoot pattern

As with the previous examples, we generally use the Monitor class with an instance of the Type class within a static method. In the same way, we will often synchronize using the this keyword within a non-static method. In both cases, we synchronize ourselves on an object which is visible from outside of the class. This can cause problems if other parts of the code synchronizes itself on these objects. To avoid such potential problems, we recommend that you use a private member named SyncRoot of type object, which is static or not depending on your needs:

Example3.cs

class Foo {
   private static object staticSyncRoot = new object();
   private object instanceSyncRoot = new object();
   public static void StaticFct() {
      lock ( staticSyncRoot ) { /*...*/ }
   }
   public void InstanceFct() {
      lock ( instanceSyncRoot ) { /*...*/ }
   }
}

The System.Collections.ICollection interface offers the object the SyncRoot{get;} property. Most of the collection classes (generic or not) implement this interface. Also, you can use this property to synchronize your access to the elements of a collection. Here, the SyncRoot pattern is not really applied since the object on which we synchronize the access is not private:

Example4.cs

using System.Collections.Generic;
using System.Collections;
public class Program {
   public static void Main() {
      List list = new List();
      // ...

      lock ( ( (ICollection) list ).SyncRoot ) {
         foreach (int i in list) {
            /* ... */
         }
      }
   }
}

Thread-safe classes

A thread-safe class is a class where each instance cannot be accessed by more than one thread at a time. To create such a thread-safe class, you simply need to apply the SyncRoot pattern that we have seen, to its methods. A good way not to burden the code of a class that we wish to be thread-safe is to provide a thread-safe wrapper derived class like this:

Example5.cs

class Foo {
   private class FooSynchronized : Foo {
      private object syncRoot = new object();
      private Foo m_Foo;
      public FooSynchronized( Foo foo ) { m_Foo = foo; }
      public override bool IsSynchronized { get { return true; } }
      public override void Fct1(){lock( syncRoot ) { m_Foo.Fct1(); } }
      public override void Fct2(){lock( syncRoot ) { m_Foo.Fct2(); } }
   }
   public virtual bool IsSynchronized { get { return false; } }
   public static Foo Synchronized(Foo foo){
      if( ! foo.IsSynchronized )
         return new FooSynchronized( foo );
      return foo;
   }
   public virtual void Fct1() { /*...*/ }
   public virtual void Fct2() { /*...*/ }
}

The Monitor.TryEnter() method

public static bool TryEnter(object [,int] )

This method is similar to Enter() but it is non-blocking. If the exclusive access rights are already owned by another thread, this method returns immediately with a return value of false. We can also make a call to TryEnter(), blocking for a limited period of time specified in milliseconds. Since the result of this method is uncertain, and that in the case where we would have acquired exclusive access rights, you have to release it in a finally clause, it is recommended to immediately exit the calling function in the case where TryEnter() failed:

Example6.cs

using System.Threading;
class Program {
   private static object staticSyncRoot = new object();
   static void Main() {
      // Comment this line to test the case where the 

      // �TryEnter()� method returns true.

      Monitor.Enter( staticSyncRoot );
      Thread t1 = new Thread(f1);
      t1.Start(); t1.Join();
   }
   static void f1() {
      if( ! Monitor.TryEnter( staticSyncRoot ) )
         return;
      try {
         // ...

      }
      finally {
         Monitor.Exit( staticSyncRoot );
      }
   }
}

The Wait(), Pulse(), and PulseAll() methods of the Monitor class

public static bool Wait( object [,int] )
public static void Pulse( object )
public static void PulseAll( object )

The Wait(), Pulse(), and PulseAll() methods must be used together, and cannot be properly understood without a small scenario. The idea is that a thread with exclusive rights to an object decides to wait (by calling Wait()) until the state of the object changes. For this, this thread must accept to temporarily lose the exclusive access rights to the object in order to allow another thread to change its state. This thread must signal such a change by using the Pulse() method. Here is a little scenario which illustrates this in more detail:

  • The T1 thread which owns exclusive access to the OBJ object, calls the Wait(OBJ) method in order to register itself in a passive wait list for the OBJ object.
  • With this call, T1 looses its exclusive access to OBJ. Hence, another thread T2 takes exclusive access to OBJ by calling Enter(OBJ).
  • T2 eventually modifies the state of OBJ and then calls Pulse(OBJ) to indicate this modification. This call causes the first thread of the passive waiting list for OBJ (in this case, T1) to move to the top of the active waiting list for OBJ. The first thread of the active list for OBJ has the guarantee that it will be the next one to have exclusive access rights to OBJ as soon as they will be released. It will then exit from its wait in the Wait(OBJ) method.
  • In our scenario, T2 releases the exclusive access rights to OBJ by calling Exit(OBJ), and T1 recovers the access rights and exits from the Wait(OBJ) method.
  • The PulseAll() method makes it so that the threads of the passive wait list all move to the active wait list. Notice that the threads are unblocked in the same order that they called Wait().

If Wait(OBJ) is called by a thread who has called Enter(OBJ) several times, this thread will need to call Exit(OBJ) the same number of times to release the access rights to OBJ. Even in this case, a single call to Pulse(OBJ) by another thread will be sufficient to unblock the first thread.

The following program illustrates this functionality through two threads ping and pong which use the access rights to a ball object in an interlaced way:

Example7.cs

using System.Threading;
public class Program {
   static object ball = new object();
   public static void Main() {
      Thread threadPing = new Thread( ThreadPingProc );
      Thread threadPong = new Thread( ThreadPongProc );
      threadPing.Start(); threadPong.Start();
      threadPing.Join();  threadPong.Join();
   }
   static void ThreadPongProc() {
      System.Console.WriteLine("ThreadPong: Hello!");
      lock ( ball )
         for (int i = 0; i < 5; i++){
            System.Console.WriteLine("ThreadPong: Pong ");
            Monitor.Pulse( ball );
            Monitor.Wait( ball );
         }
      System.Console.WriteLine("ThreadPong: Bye!");
   }
   static void ThreadPingProc() {
      System.Console.WriteLine("ThreadPing: Hello!");
      lock ( ball )
         for(int i=0; i< 5; i++){
            System.Console.WriteLine("ThreadPing: Ping ");
            Monitor.Pulse( ball );
            Monitor.Wait( ball );
         }
      System.Console.WriteLine("ThreadPing: Bye!");
   }
}

This program displays the following (in a non-deterministic way):

ThreadPing: Hello!
ThreadPing: Ping
ThreadPong: Hello!
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Bye!

The pong thread does not end and remains blocked on the Wait() method. This results from the fact that the pong thread has obtained exclusive access rights on the ball object in the second place.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here