DEV Community

Rasul
Rasul

Posted on • Edited on

Under The Hood Of Thread Synchronization With LOCK

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
        }       
    }     
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)