3

I have this C# code:

public class Locking
{

    private int Value1; private int Value2;

    private object lockValue = new Object();
    public int GetInt1(int value1, int value2)
    {
        lock (lockValue)
        {
            Value1 = value1;
            Value2 = value2;
            return GetResult();
        }

    }

    public int GetInt2(int value1, int value2)
    {
        lock (lockValue)
        {
            return GetInt1(value1, value2);
        }
    }

    private int GetResult()
    {
        return Value1 + Value2;
    }


}

So basically I expect a deadlock if I execute GetInt2 but the code just executes. Any good explanation.

Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189

3 Answers3

13

The lock blocks the executing thread, unless that thread already holds the lock on the object.

In this case, there is only one thread executing; it takes the lock on lockValue in GetInt2, then proceeds into GetInt1, where it encounters a lock statement on lockValue again - which it already holds, so it is allowed to proceed.

RJ Lohan
  • 6,497
  • 3
  • 34
  • 54
  • 3
    Yes - The important point is that the *same thread* is allowed to enter any lock that it already holds, any number of times. – Matthew Watson Jun 18 '13 at 10:09
7

The lock statement in C# is syntactic sugar, interpreted by the compiler as a call to Monitor.Enter. It's documented (in the "Monitors" section) that

lock (x)
{
    DoSomething();
}

is equivalent to

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

The documentation for Monitor.Enter states that

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.

It's obvious from the above that the given code will produce no deadlock as long as only one thread is involved.

Jon
  • 428,835
  • 81
  • 738
  • 806
5

The general case here is whether or not a synchronization object is re-entrant. In other words, can be acquired again by the same thread if it already owns the lock. Another way to say that is whether the object has "thread affinity".

In .NET, the Monitor class (which implements the lock statement), Mutex and ReaderWriterLock are re-entrant. The Semaphore and SemaphoreSlim classes are not, you could get your code to deadlock with a binary semaphore. The cheapest way to implement locking is with Interlocked.CompareExchange(), it would also not be re-entrant.

There is an extra cost associated with making a sync object re-entrant, it needs to keep track of which thread owns it and how often the lock was acquired on the owning thread. Which requires storing Thread.ManagedId and a counter, two ints. This affected choices in C++ for example, the C++11 language specification finally adding threading to the standard library. The std::mutex class is not re-entrant in that language and proposals to add a recursive version were rejected. They considered the overhead of making it re-entrant too high. A bit heavy-handed perhaps, a cost that's rather miniscule against the amount of time spent on debugging accidental deadlock :) But it is a language where it is no slamdunk that acquiring the thread ID can be guaranteed to be cheap like it is in .NET.

This is exposed in the ReaderWriterLockSlim class, you get to choose. Note the RecursionPolicy property, allowing you to choose between NoRecursion and SupportsRecursion. The NoRecursion mode is cheaper and makes it truly slim.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Semaphores work as I intended, here is my code http://stackoverflow.com/a/17194374/98491 It is just `private static ss = new SemaphoreSlim(1, 1);` and in code I wrapped `ss.Wait();` and `ss.Release()` into a try/finally block. – Jürgen Steinblock Jun 19 '13 at 14:50
  • Great answer! +1 for explaining how all locks work, not just the one asked about – Thresh Mar 18 '14 at 08:12