Lock (Monitor) Internal Implementation in .Net

Lock (Monitor) internal implementation in .NET

After some investigations I've found out answers to my questions. In general CodeInChaos and Henk Holterman were right, but here is some details.

When thread start to contends for a lock with other threads firstly it it does spin-wait loop for a while trying to obtain lock. All this actions performs in user-mode. Then if no success OS kernel object Event creates, thread is switched to the kernel-mode and waits for signal from this Event.

So answer to my questions are:

1. In better case no, but in worse yes (Event object lazily creates if required);

2. In general it works in user-mode but if threads compete for a lock too long, thread could be switched to kernel-mode (via Win API unmanaged function call);

3. Overhead for switch from user-mode to kernel-mode (~1000 CPU cycles);

4. Microsoft claim that it is "honest" algorithm like FIFO but it doesn't guarantee this. (E.g. If thread from 'waiting queue' will be suspended it moves to the end of queue when it would be resumed.)

How does `lock` (Monitor) work in .NET?

Monitor.Enter is not a normal .NET method (can't be decompiled with ILSpy or similar). The method is implemented internally by the CLR, so strictly speaking, there is no one answer for .NET as different runtimes can have different implementations.

All objects in .NET have an object header containing a pointer to the type of the object, but also an SyncBlock index into a SyncTableEntry. Normally that index is zero/non used, but when you lock on the object it will contain an index into the SyncTableEntry which then contains the reference to the actual lock object.

So locking of thousands of objects will indeed create a lot of locks which is an overhead.

The information I found was in this MSDN article: http://msdn.microsoft.com/en-us/magazine/cc163791.aspx

When exactly does .NET Monitor go to kernel-mode?

Most of these kind of questions can be answered by looking at the CLR source code as available through the SSCLI20 distribution. It is getting pretty dated by now. It is .NET 2.0 vintage, but a lot of the core CLR features haven't changed much.

The source code file you want to look at is clr/src/vm/syncblk.cpp. Three classes play a role here. AwareLock is the low-level lock implementation that takes care of acquiring the lock, SyncBlock is the class that implements the queue of threads that are waiting to enter a lock, and CLREvent is the wrapper for the operating system synchronization object, the one you are asking about.

This is C++ code and the level of abstraction is quite high. This code heavily interacts with the garbage collector and there's a lot of testing code included. So I'll give a brief description of the process.

SyncBlock has the m_Monitor member that stores the AwareLock instance. SyncBlock::Enter() directly calls AwareLock::Enter(). It first tries to acquire the lock as cheaply as possible. First checking if the thread already owns the lock and just incrementing the lock count if that's the case. Next using FastInterlockCompareExchange(), an internal function that's very similar to Interlocked.CompareExchange(). If the lock is not contended then this succeeds very quickly and Monitor.Enter() returns. If not then another thread already owns the lock, and AwareLock::EnterEpilog is used. There's a need to get the operating system's thread scheduler involved so a CLREvent is used. It is dynamically created if necessary and its WaitOne() method is called. Which will involve a kernel transition.

So there is enough to answer your question: the Monitor class enters kernel mode when the lock is contended and the thread has to wait.

Contention on a Resource - not always using lock?

So , according to the info above , if there no contention , the lock wont be used ? (what if another thread is about to enter 1 ms after?)

Correct. Then there's contention, and the other thread will have to enter the kernel. Also, the thread that has the lock will also have to enter the kernel when it unlocks it.

The operations kind of look like this:

Lock:

  1. Try to atomically set the user-space lock variable from unlock to lock. If we do, stop, we're done.

  2. Increment the user-space contention count.

  3. Set the kernel-space lock to locked.

  4. Try to atomically set the user-space lock variable from unlock to lock. If we do, decrement the user-space contention count and stop, we're done.

  5. Block in the kernel on the kernel lock.

  6. Go to step 3.

Unlock:

  1. Atomically set the user-space lock variable from lock to unlock.

  2. If the user-space contention count is zero, stop, we're done.

  3. Set the kernel lock to unlocked.

Notice how if there's no contention, the lock operation only involves step 1 and the unlock operation only involves steps 1 and 2, all of which take place in user space.

How does System.Threading.Monitor.Enter() work?

Looking at the Mono source code, it seems that they create a Semaphore (using CreateSemaphore or a similar platform-specific function) when the object is first locked, and store it in the object. There also appears to be some object pooling going on with the semaphores and their associated MonoThreadsSync structures.

The relevant function is static inline gint32 mono_monitor_try_enter_internal (MonoObject *obj, guint32 ms, gboolean allow_interruption) in the file mono/metadata/monitor.c, in case you're interested.

I expect that Microsoft .Net does something similar.

Monitor vs lock

Eric Lippert talks about this in his blog:
Locks and exceptions do not mix

The equivalent code differs between C# 4.0 and earlier versions.


In C# 4.0 it is:

bool lockWasTaken = false;
var temp = obj;
try
{
Monitor.Enter(temp, ref lockWasTaken);
{ body }
}
finally
{
if (lockWasTaken) Monitor.Exit(temp);
}

It relies on Monitor.Enter atomically setting the flag when the lock is taken.


And earlier it was:

var temp = obj;
Monitor.Enter(temp);
try
{
body
}
finally
{
Monitor.Exit(temp);
}

This relies on no exception being thrown between Monitor.Enter and the try. I think in debug code this condition was violated because the compiler inserted a NOP between them and thus made thread abortion between those possible.

Monitor.TryEnter()

Look at this question, I think it will very useful for you - Does lock() guarantee acquired in order requested?

especially this quote:

Because monitors use kernel objects internally, they exhibit the same
roughly-FIFO behavior that the OS synchronization mechanisms also
exhibit (described in the previous chapter). Monitors are unfair, so
if another thread tries to acquire the lock before an awakened waiting
thread tries to acquire the lock, the sneaky thread is permitted to
acquire a lock.

Lock statement vs Monitor.Enter method

It is because the reference pointed to by test1 is assigned to the local variable CS$2$0000 in the IL code. You null out the test1 variable in C#, but the lock construct gets compiled in such a manner that a separate reference is maintained.

It is actually quite clever that the C# compiler does this. Otherwise it would be possible to circumvent the guarentee the lock statement is supposed to enforce of releasing the lock upon exiting the critical section.

How does System.Threading.Monitor.Enter() work?

Looking at the Mono source code, it seems that they create a Semaphore (using CreateSemaphore or a similar platform-specific function) when the object is first locked, and store it in the object. There also appears to be some object pooling going on with the semaphores and their associated MonoThreadsSync structures.

The relevant function is static inline gint32 mono_monitor_try_enter_internal (MonoObject *obj, guint32 ms, gboolean allow_interruption) in the file mono/metadata/monitor.c, in case you're interested.

I expect that Microsoft .Net does something similar.

What does a lock statement do under the hood?

The lock statement is translated by C# 3.0 to the following:

var temp = obj;

Monitor.Enter(temp);

try
{
// body
}
finally
{
Monitor.Exit(temp);
}

In C# 4.0 this has changed and it is now generated as follows:

bool lockWasTaken = false;
var temp = obj;
try
{
Monitor.Enter(temp, ref lockWasTaken);
// body
}
finally
{
if (lockWasTaken)
{
Monitor.Exit(temp);
}
}

You can find more info about what Monitor.Enter does here. To quote MSDN:

Use Enter to acquire the Monitor on
the object passed as the parameter. If
another thread has executed an Enter
on the object but has not yet executed
the corresponding Exit, the current
thread will block until the other
thread releases the object. It is
legal for the same thread to invoke
Enter more than once without it
blocking; however, an equal number of
Exit calls must be invoked before
other threads waiting on the object
will unblock.

The Monitor.Enter method will wait infinitely; it will not time out.



Related Topics



Leave a reply



Submit