In this post, I will try to briefly explain how locking mechanisms provided by .NET work and how CLR manages to keep track of locks and do the synchronization, allowing us to achieve mutual exclusion.

TL;DR: Every reference type instance has some extra fields in memory, one of which can be used to store (or at least point to) the information that can indicate whether some thread has acquired a lock on that object. CLR looks that up before allowing the thread to acquire a lock on the object.

Consider the following code

class Foo
{
    private static readonly object _lock = new object();
    
    public void DoSomething()
    {
        lock(_lock)
        {
            //Do some work on shared resources
        }
    }
}

which is the same as

class Foo
{
    private static readonly object _lock = new object();

    public void DoSomething()
    {
        Monitor.Enter(_lock);
        try
        {
            // Do some work on shared resources
        }
        finally
        {
            Monitor.Exit(_lock);
        }
    }
}

Only a single thread at a time can access the shared resources using a DoSomething method. If another thread tries to access the shared resources using DoSomething method, it will be blocked until first thread releases the lock. But how does this work?

Let’s keep in mind one thing: Lock can be acquired on reference type instances only.

Memory Layout

Now let’s take a look at memory layouts for reference and value type instances on a 32bit (x86) CLR:

memory-layouts

As you can see, the instance of a value type contains just the values of its fields. Reference type instance, on the other side, contains two overhead fields:

  • RTTI (RunTime Type Information) Reference: This field contains the address of a type object which contains method table. Method table is used by CLR to obtain the address of the actual implementation when a virtual method is called.

  • Object Header: This field is used by CLR to store some additional information. It can contain information like object’s hash code or whether the finalizer for an object has been run, but it can also contain an object’s lock state.

After looking at memory layouts we can see why we can’t lock, derive or write a finalizer for value types.

Now, let’s see how CLR stores and retrieves lock state using the object header.

If there is room on the object header, CLR stores the managed thread ID of the thread that currently holds the lock on the object (or zero (0) if no thread holds the lock). This enables simple lock acquiring by spin-waiting until the object header's thread ID is zero, and then atomically setting it to the current thread's managed thread ID.
However, there are some situations when CLR will not store the lock state in the object header. Some of those reasons are:

  • There is no room on the object header because it is already being used for other purposes (as we mentioned above, object header can also be used for object hash, information about finalizing etc.)
  • The lock cannot be acquired after some number of spins.
  • Monitor.Wait and Monitor.Pulse are used, so condition variable needs to be stored

Since it cannot always store lock state on the object header, CLR needs to have other ways of storing this information.

Sync Block Table

When the CLR initializes, it allocates a Sync Block Table in native heap. This table contains sync blocks which are addressed by their indexes. If lock state cannot be stored in the object header, a sync block is created and lock state is stored in it and object header only stores the sync block index. Sync block can store additional data, including an event that can be used to block the current thread, allowing us to stop spinning and efficiently wait for the lock to be released.

So, sync block is not stored in object. CLR maintains sync block table separately and the object only contains an array index of sync block. Also, sync block table is able to create more sync blocks if necessary, so you shouldn’t worry about the system running out of sync blocks if many objects are being synchronized simultaneously.

CoreCLR implementation of sync blocks can be found in syncblk.h and syncblk.cpp files.

Mutual Exclusion

When Monitor.Enter is called, the CLR registers the lock acquisition either by storing the current thread ID in the object header or by creating the sync block in the sync block table and storing its index in the object header. Other threads that try to acquire the lock on the object will have to wait until the Monitor.Exit is called in the thread that holds the lock. When Exit is called, it checks to see whether there are any more threads waiting to acquire lock on the object. If there are no threads waiting for it, Exit clears the lock information from the object header.