Skip to Content
Course content

196: Monitor and Lock Internals

Click on the "Edit" button in the top corner of the screen to edit your slide content.

What is actually happening under the hood when I use the lock keyword?

You've probably used lock(obj) { ... } a hundred times, but it's important to realize that lock is actually just "syntactic sugar." When the compiler sees that keyword, it expands it into a try-finally block using Monitor.Enter and Monitor.Exit.

But where does the "lock" actually live? It's not a property you can see in the debugger. In .NET, every object has an "object header" in memory. This header contains a bit that tells the CLR if the object is currently synchronized. If it is, the header points to a SyncBlock. The SyncBlock is a structure maintained by the runtime that tracks which thread owns the lock and which threads are currently waiting in line (the wait queue).

// This is what you write:
lock (_syncRoot) 
{
    _count++;
}

// This is effectively what the compiler generates:
bool lockTaken = false;
try 
{
    Monitor.Enter(_syncRoot, ref lockTaken);
    _count++;
} 
finally 
{
    if (lockTaken) Monitor.Exit(_syncRoot);
}

Why should I bother using the Monitor class directly if lock does the same thing?

For 90% of your mutual exclusion needs, lock is fine. But Monitor gives you two superpowers that lock doesn't: timeouts and signaling.

First, there's Monitor.TryEnter. If you're worried about deadlocks, you don't want a thread to hang forever. With TryEnter, you can say, "Try to get the lock for 500ms; if you can't, I'll give up and try something else."

Second, and more importantly, you have Wait and Pulse. This is how you coordinate threads. Imagine a producer-consumer scenario: a consumer thread shouldn't just loop and check if a queue has items (that wastes CPU). Instead, it calls Monitor.Wait, which puts the thread to sleep and releases the lock. When the producer adds an item, it calls Monitor.Pulse, which wakes up one of the sleeping threads to tell it, "Hey, there's work to do now!"

Do I really need a dedicated lock object, or can I just lock on 'this'?

I see people locking on this or on a public property all the time, and frankly, it's a recipe for disaster. Here is why: you have no control over who else is locking on your object.

If you expose your object publicly and some other developer decides to lock on that same instance in a different part of the codebase, you can end up with a deadlock that is nearly impossible to trace. By using a private readonly object _lock = new();, you ensure that the lock is internal to your class. No one outside your class can interfere with your synchronization logic.

Also, never ever lock on a string. Because of string interning, two different variables containing the same string literal actually point to the same object in memory. You could accidentally be locking on a global object shared by the entire AppDomain, which will absolutely tank your performance or freeze your app.




📋 Practical Task

Implementing a Signaling Message Queue with Monitor.Wait and Pulse

Build a class called SignalingQueue that allows one thread to produce a message and another to consume it, without using ConcurrentQueue or BlockingCollection. You must use Monitor.Wait and Monitor.Pulse to handle the synchronization.

  • Create a private Queue<string> and a private lock object.
  • Implement an Enqueue(string message) method that adds a message to the queue and calls Monitor.Pulse to notify a waiting consumer.
  • Implement a Dequeue() method that checks if the queue is empty. If it is, it must call Monitor.Wait to sleep until a message is produced. Once woken and a message is available, it should remove and return the message.
  • Test your implementation by starting a consumer thread that calls Dequeue in a loop, and a producer thread that adds messages every 2 seconds. Verify that the consumer thread sleeps efficiently and only wakes up when a message actually arrives.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.