Practical .NET2 and C#2: Chapter 5






3.63/5 (14 votes)
Mar 16, 2006
7 min read

39364

174
This chapter describes the System.Threading.Monitor class and the lock C# keyword.
|
The following article is an excerpt from chapter 5 of the book Practical .NET2 and C#2.
Contents
- The Enter() and Exit() methods
- The lock C# keyword
- The SyncRoot pattern
- Thread-safe classes
- The Monitor.TryEnter() method
- The Wait(), Pulse(), and PulseAll() methods of the Monitor class
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 callsExit()
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()
andExit()
methods on an instance of a value type such as an integer! - Do call the
Exit()
in afinally
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 theOBJ
object, calls theWait(OBJ)
method in order to register itself in a passive wait list for theOBJ
object. - With this call,
T1
looses its exclusive access toOBJ
. Hence, another threadT2
takes exclusive access toOBJ
by callingEnter(OBJ)
. T2
eventually modifies the state ofOBJ
and then callsPulse(OBJ)
to indicate this modification. This call causes the first thread of the passive waiting list forOBJ
(in this case,T1
) to move to the top of the active waiting list forOBJ
. The first thread of the active list forOBJ
has the guarantee that it will be the next one to have exclusive access rights toOBJ
as soon as they will be released. It will then exit from its wait in theWait(OBJ)
method.- In our scenario,
T2
releases the exclusive access rights toOBJ
by callingExit(OBJ)
, andT1
recovers the access rights and exits from theWait(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 calledWait()
.
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.