Friday, February 14, 2014

How does locking work in C#?

A question I've gotten from several readers recently is "how does the lock statement actually work in C#?" It appears to be somewhat magical; if you didn't already have a lock statement, how would you implement it?

Before I go on, I should make one thing very clear: this article is going to be chock full of lies. The mechanisms that the CLR actually uses to implement locks are quite a bit more complicated than the oversimplified sketch I'm presenting here. The intended takeaway here is not that you understand precisely what the CLR does, but rather that you understand how a very simple lock could be built out of even simpler parts.

Let's start by clearly describing what a lock statement does. It is documented by the specification as having two main properties. First, a lock statement takes a reference to an object which may be "acquired" by a thread; such an object is called a "monitor" for historical reasons. Only one thread may acquire a particular monitor at one time. If a second thread attempts to acquire a particular monitor while a first thread is holding it, the second thread blocks until such time as the first thread releases the lock and the second thread can acquire the monitor. The question then is how this behaviour can be implemented without locking, since that's what we're trying to implement. Second, the C# specification states that certain special side effects in multithreaded programs are always observed to be ordered in a particular way with respect to locks; we won't discuss this aspect of locking in this article.

Once more, before I go on I want to clarify a few other differences between what I'm presenting today and reality. In the real C# language you can lock on any instance of a reference type; we won't consider that. In the real C# the same thread can acquire the same monitor twice:

void M()
{
  lock(this.someMonitor) { N(); }
}
 
void N()
{
  lock(this.someMonitor) { whatever }
}
We won't consider that either. And in the real C# language, there are more operations you can perform on monitors than just acquiring and releasing them, such as waiting and pulsing. I'm not going to discuss any of these today; remember, this is a pile of lies intended to get across the idea that locks are built out of more fundamental parts. The real implementation is far more complex than what I'll present here.

OK, now that we've got that out of the way, the next thing to discuss is what the lock statement actually means in C#. When you say

void N()
{
  lock(this.someMonitor) { whatever }
}
the C# compiler does a very simple transformation of that code into:

void N()
{
  object monitor = this.someMonitor;
  System.Threading.Monitor.Enter(monitor);
  try
  {
    whatever
  }
  finally
  {
    System.Threading.Monitor.Exit(monitor);
  }
}

Read more: Coverity