Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Under what conditions can a thread enter a lock (Monitor) region more than once concurrently?

(question revised): So far, the answers all include a single thread re-entering the lock region linearly, through things like recursion, where you can trace the steps of a single thread entering the lock twice. But is it possible somehow, for a single thread (perhaps from the ThreadPool, perhaps as a result of timer events or async events or a thread going to sleep and being awaken/reused in some other chunk of code separately) to somehow be spawned in two different places independently of each other, and hence, run into the lock re-entrance problem when the developer didn't expect it by simply reading their own code?

In the ThreadPool Class Remarks (click here) the Remarks seem to suggest that sleeping threads should be reused when they're not in use, or otherwise wasted by sleeping.

But on the Monitor.Enter reference page (click here) they say "It is legal for the same thread to invoke Enter more than once without it blocking." So I figure there must be something I'm supposed to be careful to avoid. What is it? How is it even possible for a single thread to enter the same lock region twice?

Suppose you have some lock region that takes an unfortunately long time. This might be realistic, for example, if you access some memory that has been paged out (or whatever.) The thread in the locked region might go to sleep or something. Does the same thread become eligible to run more code, which might accidentally step into the same lock region? The following does NOT, in my testing, get multiple instances of the same thread to run into the same lock region.

So how does one produce the problem? What exactly do you need to be careful to avoid?

class myClass
{
    private object myLockObject;
    public myClass()
    {
        this.myLockObject = new object();
        int[] myIntArray = new int[100];               // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Array.Clear(myIntArray, 0, myIntArray.Length); // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Parallel.ForEach<int>(myIntArray, i => MyParallelMethod());
    }
    private void MyParallelMethod()
    {
        lock (this.myLockObject)
        {
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " starting...");
            Thread.Sleep(100);
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " finished.");
        }
    }
}
like image 813
Edward Ned Harvey Avatar asked Dec 21 '12 03:12

Edward Ned Harvey


1 Answers

Suppose you have a queue that contains actions:

public static Queue<Action> q = whatever;

Suppose Queue<T> has a method Dequeue that returns a bool indicating whether the queue could be successfully dequeued.

And suppose you have a loop:

static void Main()
{
    q.Add(M);
    q.Add(M);
    Action action;
    while(q.Dequeue(out action)) 
      action();
}
static object lockObject = new object();
static void M()
{
    Action action;
    lock(lockObject) 
    { 
        if (q.Dequeue(out action))
            action();
    }
}

Clearly the main thread enters the lock in M twice; this code is re-entrant. That is, it enters itself, through an indirect recursion.

Does this code look implausible to you? It should not. This is how Windows works. Every window has a message queue, and when a message queue is "pumped", methods are called corresponding to those messages. When you click a button, a message goes in the message queue; when the queue is pumped, the click handler corresponding to that message gets invoked.

It is therefore extremely common, and extremely dangerous, to write Windows programs where a lock contains a call to a method which pumps a message loop. If you got into that lock as a result of handling a message in the first place, and if the message is in the queue twice, then the code will enter itself indirectly, and that can cause all manner of craziness.

The way to eliminate this is (1) never do anything even slightly complicated inside a lock, and (2) when you are handling a message, disable the handler until the message is handled.

like image 82
Eric Lippert Avatar answered Sep 20 '22 00:09

Eric Lippert