Under The Hood Of Thread Synchronization With LOCK
Rasul
Posted on May 15, 2024
In the previous article, we talked about “Introduction to Monitor Class In C#”. In this article, we will discuss the LOCK statement.
The LOCK statement is the most popular mutual-exclusive thread synchronization construct. You will be surprised when we deep dive into details.
LOCK Statement
Basically, LOCK is a reserved keyword that is recognized by the C# compiler. It provides thread synchronization for a shared resource in case of execution in the critical section area and helps to avoid problems related to race conditions.
In order to use LOCK, you need to define a critical section area and wrap a LOCK around it. This will prevent threads from accessing shared resources at the same time.
How to use the LOCK statement is shown in the code below.
public class Program
{
private static readonly 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.Read();
}
static void IncrementTheValue()
{
// Critical section start point
lock (_LOCKREF)
{
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
}
}
As you can see, the above code prevents the race condition problem by wrapping the LOCK statement around the critical section area. Also, it is best to use a private reference type to provide a lock state (_LOCKREF). So do not use public reference instance, the ‘this’ keyword or interned string type; They may cause a deadlock.
Let’s understand how it works basically; Threads 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.
Under The Hood
In fact, the LOCK statement is syntactic sugar. This means that when the C# compiler compiles code into the IL language, some keywords are converted into more complex code. In this way, it provides efficiency (providing ease of use).
In the code below you can see our IL (Microsoft Intermediate Language) code. It is very interesting that LOCK is converted to a Monitor class!
.method private hidebysig static void IncrementTheValue() cil managed
{
// Code size 206 (0xce)
.maxstack 2
.locals init (object V_0,
bool V_1,
valuetype [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_2)
IL_0000: ldsfld object Program::_LOCKREF
IL_0005: stloc.0
IL_0006: ldc.i4.0
IL_0007: stloc.1
.try
{
IL_0008: ldloc.0
IL_0009: ldloca.s V_1
IL_000b: call void [System.Threading]System.Threading.Monitor::Enter(object,
bool&)
IL_0010: ldsfld int32 Program::_sharedField
IL_0015: ldc.i4.5
IL_0016: bge.s IL_0077
IL_0018: ldc.i4.s 55
IL_001a: call void [System.Threading.Thread]System.Threading.Thread::Sleep(int32)
IL_001f: ldsfld int32 Program::_sharedField
IL_0024: ldc.i4.1
IL_0025: add
IL_0026: stsfld int32 Program::_sharedField
IL_002b: ldc.i4.s 18
IL_002d: ldc.i4.2
IL_002e: newobj instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32,
int32)
IL_0033: stloc.2
IL_0034: ldloca.s V_2
IL_0036: ldstr "Field: "
IL_003b: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_0040: ldloca.s V_2
IL_0042: ldsfld int32 Program::_sharedField
IL_0047: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_004c: ldloca.s V_2
IL_004e: ldstr " | Thread: "
IL_0053: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_0058: ldloca.s V_2
IL_005a: call class [System.Threading.Thread]System.Threading.Thread [System.Threading.Thread]System.Threading.Thread::get_CurrentThread()
IL_005f: callvirt instance int32 [System.Threading.Thread]System.Threading.Thread::get_ManagedThreadId()
IL_0064: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_0069: ldloca.s V_2
IL_006b: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear()
IL_0070: call void [System.Console]System.Console::WriteLine(string)
IL_0075: leave.s IL_00cd
IL_0077: ldc.i4.s 41
IL_0079: ldc.i4.2
IL_007a: newobj instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32,
int32)
IL_007f: stloc.2
IL_0080: ldloca.s V_2
IL_0082: ldstr "Field count is complited "
IL_0087: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_008c: ldloca.s V_2
IL_008e: ldsfld int32 Program::_sharedField
IL_0093: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_0098: ldloca.s V_2
IL_009a: ldstr " | Thread Id is "
IL_009f: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_00a4: ldloca.s V_2
IL_00a6: call class [System.Threading.Thread]System.Threading.Thread [System.Threading.Thread]System.Threading.Thread::get_CurrentThread()
IL_00ab: callvirt instance int32 [System.Threading.Thread]System.Threading.Thread::get_ManagedThreadId()
IL_00b0: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_00b5: ldloca.s V_2
IL_00b7: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear()
IL_00bc: call void [System.Console]System.Console::WriteLine(string)
IL_00c1: leave.s IL_00cd
} // end .try
finally
{
IL_00c3: ldloc.1
IL_00c4: brfalse.s IL_00cc
IL_00c6: ldloc.0
IL_00c7: call void [System.Threading]System.Threading.Monitor::Exit(object)
IL_00cc: endfinally
} // end handler
IL_00cd: ret
} // end of method Program::IncrementTheValue
If the above IL code is not clear, you can check line 16, which declares System.Threading.Monitor::Enter(object, bool&). As you can see the LOCK statement is converting to Monitor class. Also the line 77 declares System.Threading.Monitor::Exit(object) which provides release lock state.
So, let’s look at the above IL code as C# code. The code declared below is similar to the code above (our previous method is wrapped by the Monitor class):
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)
{
Monitor.Exit(_LOCKREF);
}
}
}
As you can see, try + finally blocks and Monitor class are used. Also, the LockTaken pattern is used to release only the locked ones (for more details).
Conclusion
The LOCK statement is a mutual-exclusive thread synchronization construct (because it uses the Monitor class). Also, this is syntactic sugar in C# that improves usability.
If you want to learn more about Monitor class, you can check it out.
Stay Tuned!
Posted on May 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.