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 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 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.
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) {
}
}
}
}
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() { }
}
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() {
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 );
}
}
}
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.