In the previous article, we have talked about race condition problem. So, in this article we will focus on one of the solutions (using critical section), the Monitor class, which is a mutual-exclusive thread synchronization construct.
The Monitor class is built on dotNET’s FCL (Framework Class Library) infrastructure. In general, it helps to achieve thread safety.
Thread Synchronization In Critical Section
Thread synchronization is used to prevent corruption when multiple threads access shared data at the same time; Because thread synchronization is all about timing.
The System.Threading.Monitor class basically helps us to avoid the race condition problem by protecting the critical section.
Let’s think that we have a code block that has a shared resource (variable or any state), and this resource is used in an executed context by multithread. So, in this case, we need to understand that the potential race condition problem exists for us. And also that code block is a critical section for us.
The Monitor class provides threads to enter one by one into the code block, i.e. it accepts the thread into the critical section and executes the block with a single thread, then releases the thread. This means that all the other threads must wait and halt the execution until the locked section is released. And this cycle will continue in the same way.
In other words, it helps us to synchronize the execution of threads between each other.
How to Use The Monitor Class?
Let’s simulate the race condition problem and then prevent to it with Monitor Class.
public class Program
{
// A basic field for the simulation of shared data
static int _sharedField = 0;
public static void Main()
{
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem((s) => IncrementTheValue());
}
Console.WriteLine("Main method is finished!");
Console.Read();
}
// The method variable will result inconsistent
static void IncrementTheValue()
{
if (_sharedField < 5)
{
Thread.Sleep(55); // just for simulation execution time!
_sharedField += 1;
Console.WriteLine($"Field: {_sharedField} | Thread: {Thread.CurrentThread.ManagedThreadId}");
}
else
{
Console.WriteLine($"Field count is complited {_sharedField} | Thread Id is {Thread.CurrentThread.ManagedThreadId}");
}
}
}
The code above shows that we have a main method that creates 10 threads, and they independently execute the IncrementTheValue method. The method steps are:
- if _sharedField is less than 5, then enter the code block
- Execution time simulation with Thread.Sleep(55) (fake operation)
- Increment the value of _sharedField
- Write into the console _sharedField value and current thread id
Well, if you run the code, you will see that the result of the _sharedField variable is above 5. So, we are caught in a race condition problem.
Hmm, let’s avoid this problem. First of all, we need to identify our critical section area, in our case, it is the IncrementTheValue method completely. Because the method uses IF and ELSE blocks. So, we need to guarantee that our IF block will not participate in the case of racing threads. Otherwise, the race condition problem will continue.
public class Program
{
// Should be reference type
// Should be private
static object _LOCKREF = new object();
// A basic field for the simulation of shared data
static int _sharedField = 0;
public static void Main()
{
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem((s) => IncrementTheValue());
}
Console.WriteLine("Main method is finished!");
Console.Read();
}
static void IncrementTheValue()
{
try
{
Monitor.Enter(_LOCKREF);
// Critical section start point
if (_sharedField < 5)
{
Thread.Sleep(55); // just for simulation execution time!
_sharedField += 1;
Console.WriteLine($"Field: {_sharedField} | Thread: {Thread.CurrentThread.ManagedThreadId}");
} // Critical section end point
else
{
Console.WriteLine($"Field count is complited {_sharedField} | Thread Id is {Thread.CurrentThread.ManagedThreadId}");
}
}
finally
{
// Ensure that the lock is released
Monitor.Exit(_LOCKREF);
}
}
}
In the code above, we see that Monitor.Enter method is declared and this method provides a wait-based (the waiting is implemented effectively, because avoids the CPU wasting) synchronization mechanism that allows only one thread to access the critical section code.
For the case of exceptions, we are using try + finally blocks. Because the Monitor class must release the lock at the end of any situation. So, we see that in the finally block has been declared Monitor.Exit method which helps us to release the lock from critical section.
As a result, with the code above, we have solved our race condition problem through the Monitor class. And now our code is thread-safe!
Input Parameter of Monitor.Enter Method
In the code above, where we avoid the race condition problem, there is an instance of the Object class its _LOCKREF which taking input into the Monitor.Enter method.
In our example code, the _LOCKREF instance helps us lock the thread. That is, it provides a reference to distinguish the context. So, you need to be careful when using Monitor.Enter method, that the reference to be used as an input parameter is closed outside the class.
So, we can understand that the input parameter of Monitor.Enter is some flag for understanding the locking state (in a simple term).
Note that if the _LOCKREF instance is manipulated or opened outside, it may cause a deadlock. Also, do not use the “this” keyword instead of _LOCKREF as it may cause deadlock, too.
LockTaken Pattern
As we can see in our code example, there is a try + finally block. So, in a multithreaded scenario, what happens if one of the threads enters the code block and an exception is thrown (or the thread is aborted) in the first step where it enters ‘try’ before reaching the Monitor.Enter method?
Hmm, I think almost everyone realized that the finally block will work in any case. So, if the thread without acquiring the lock tries to release the lock; it will receive System.Threading.SynchronizationLockException exception. Well, to avoid any case of this situation we need to use the LockTaken Pattern.
static void IncrementTheValue()
{
bool lockTaken = false;
try
{
Monitor.Enter(_LOCKREF, ref lockTaken);
// Critical section start point
if (_sharedField < 5)
{
Thread.Sleep(55); // just for simulation execution time!
_sharedField += 1;
Console.WriteLine($"Field: {_sharedField} | Thread: {Thread.CurrentThread.ManagedThreadId}");
}
else
{
Console.WriteLine($"Field count is complited {_sharedField} | Thread Id is {Thread.CurrentThread.ManagedThreadId}");
}
// Critical section end point
}
finally
{
if (lockTaken)
{
// Ensure that the lock is released
Monitor.Exit(_LOCKREF);
Console.WriteLine($"Thread Id is {Thread.CurrentThread.ManagedThreadId} released from the lock!");
}
}
}
To implement this pattern, the Monitor.Enter method has an overload, it accepts 2 parameters. One of them is the lock reference instance; the other accepts a boolean variable.
As we see, in the first line of the code above, there is a declared boolean variable lockTaken.
Monitor.Enter accepts the lockTaken variable with ref keyword. This means that if the method is true, the lock is taken and if false, the lock is not taken. So, finally block handles by checking lockTaken variable and in case of true if block executes otherwise it does not.
The lockTaken pattern provides safety in case any thread is aborted without acquiring a lock.
Methods of The Monitor Class
Not all methods are described. You can check them all in Microsoft Documentation.
- Enter(Object): Acquires an exclusive lock on the specified object.
- Enter(Object, Boolean): Acquires an exclusive lock on the specified object, and atomically sets a value that indicates whether the lock was taken.
- Exit(Object): Releases an exclusive lock on the specified object.
- IsEntered(Object): Determines whether the current thread holds the lock on the specified object.
- Pulse(Object): Notifies a thread in the waiting queue of a change in the locked object’s state.
- PulseAll(Object): Notifies all waiting threads of a change in the object’s state.
- TryEnter(Object): Attempts to acquire an exclusive lock on the specified object.
- TryEnter(Object, Boolean): Attempts to acquire an exclusive lock on the specified object, and atomically sets a value that indicates whether the lock was taken.
- Wait(Object): Releases the lock on an object and blocks the current thread until it reacquires the lock.
- Wait(Object, Int32): Releases the lock on an object and blocks the current thread until it reacquires the lock. If the specified time-out interval elapses, the thread enters the ready queue.
In the next article, I will write about “Deep Dive Into Monitor Class in .NET”. Stay tuned!