Under what conditions can a thread enter the blocking area (Monitor) more than once at a time?

(question revised): So far, all answers include one thread, which returns to the blocking area linearly, through things like recursion, where you can trace the steps of one thread entering the lock twice. But is it possible somehow for one thread (perhaps from ThreadPool, perhaps due to timer events or asynchronous events or a thread that will sleep and wake up / reuse in another piece of code separately) to somehow be generated are two different places independent of each other and, therefore, faced with the problem of re-entering the castle when the developer did not expect this, just reading his own code?

In the ThreadPool class note ( click here ). Notes seem to suggest that sleeping threads should be reused when not in use, or otherwise lost in hibernation.

But on the Monitor.Enter help page ( click here ), they say: "It is legal for the same thread to call Enter more than once without blocking it." Therefore, I believe that there should be something that I should avoid. What is it? How is it possible for one thread to enter the same blocking area twice?

Suppose you have a blocking area, which, unfortunately, is a long time. This can be realistic, for example, if you are accessing some memory that has been unloaded (or something else.) A thread in a locked area may be sleeping or something like that. Does the same thread have the ability to run more code that could accidentally enter the same blocking area? In my testing, there are NOT multiple instances of the same thread that will run in the same blocking area.

So how does the problem arise? 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."); } } } 
+8
c # concurrency thread-safety locking
source share
6 answers

ThreadPool threads cannot be reused elsewhere just because they slept; they must finish before they are reused. A thread that takes a long time in the blocking area does not have the right to run more code at any other independent control point. The only way to re-enter a lock is by recursing or executing methods or delegates inside the lock that re-enter the lock.

+1
source share

Suppose you have a queue containing actions:

 public static Queue<Action> q = whatever; 

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

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(); } } 

It is clear that the main thread enters the castle in M ​​twice; this code is repeated. That is, it introduces itself through indirect recursion.

Does this code look implausible to you? It should not. This is how Windows works . Each window has a message queue, and when the message queue is "pumped up", methods corresponding to these messages are called. When you click the button, the message is sent to the message queue; when the queue is pumped up, the click handler corresponding to this message is called.

Therefore, it is extremely often and extremely dangerous to write Windows programs in which a lock contains a method call that inflates a message loop. If you get into this lock as a result of processing the message in the first place, and if the message is in the queue twice, then the code will enter itself indirectly, and this can cause all kinds of crazy things.

The way to fix this: (1) never does anything complicated inside the lock and (2) when you process the message, turn off the handler until the message is processed.

+12
source share

Re-Entrance is possible if you have this structure:

 Object lockObject = new Object(); void Foo(bool recurse) { lock(lockObject) { Console.WriteLine("In Lock"); if (recurse) { foo(false); } } } 

Although this is a fairly simple example, it is possible in many scenarios where you have interdependent or recursive behavior.

For example:

  • ComponentA.Add (): blocks the common ComponentA object, adds a new element to ComponentB.
  • ComponentB.OnNewItem (): a new item starts data validation for each item in the list.
  • ComponentA.ValidateItem (): Locks the shared ComponentA object to validate the item.

Re-recording with the same thread with the same lock is necessary to ensure that you do not receive deadlocks associated with your own code.

+5
source share

One of the most subtle ways you can perform in a blocking block is through graphical interfaces. For example, you can asynchronously invoke code in a single user interface thread (form class)

 private object locker = new Object(); public void Method(int a) { lock (locker) { this.BeginInvoke((MethodInvoker) (() => Method(a))); } } 

Of course, this also puts an endless loop; you are likely to have a condition by which you want to recurs at what point you will not have an infinite loop.

Using lock not a good way to sleep / wake threads. I would simply use existing frameworks such as the Task Parallel Library (TPL) to simply create abstract tasks (see Task ) to create, and the base environment handles the creation of new threads and sleeps if necessary.

+4
source share

IMHO, Re-entering a lock is not what you need to take care to avoid (given that for many people, the mental lock model is dangerous at best, see Change below). The point of the documentation is to explain that the thread cannot block itself using Monitor.Enter . This does not always apply to all synchronization mechanisms, frameworks, and languages. Some have non-ad-hoc synchronization, in which case you should be careful that the thread does not block itself. What you need to be careful always calls Monitor.Exit for every call to Monitor.Enter . The lock keyword does this for you automatically.

Trivial re-entry example:

 private object locker = new object(); public void Method() { lock(locker) { lock(locker) { Console.WriteLine("Re-entered the lock."); } } } 

The thread entered the lock twice on the same object, so it must be released twice. Usually this is not so obvious, and there are various methods that call each other, which are synchronized on the same object. The fact is that you do not need to worry about blocking the stream.

However, you should try to minimize the amount of time it takes to lock. Acquiring a lock is not expensive computing, contrary to what you can hear (it is on the order of a few nanoseconds). Lair rivalry is that expensive.

Edit

Please read Eric’s comments below for more information, but the summary is that when you see lock , your interpretation should be that “all activations of this code block are associated with one thread” and not as it is usually interpreted , "all activations of this code block are performed as a single atomic unit."

For example:

 public static void Main() { Method(); } private static int i = 0; private static object locker = new object(); public static void Method() { lock(locker) { int j = ++i; if (i < 2) { Method(); } if (i != j) { throw new Exception("Boom!"); } } } 

Obviously, this program is exploding. Without lock this is the same result. The danger is that lock leads you into a false sense of security that nothing can change the state on you between initializing j and evaluating if . The problem is that you (possibly inadvertently) have a Method recursive into itself, and lock will not stop this. As Eric points out in his answer, you may not be aware of this problem until one day someone puts in line too many actions at the same time.

+4
source share

Think of something other than recursion.
In some business logic, they would like to control synchronization behavior. One of these patterns, they call Monitor.Enter somewhere and would like to call Monitor.Exit elsewhere later. Here is the code to get an idea about this:

 public partial class Infinity: IEnumerable<int> { IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public IEnumerator<int> GetEnumerator() { for(; ; ) yield return ~0; } public static readonly Infinity Enumerable=new Infinity(); } public partial class YourClass { void ReleaseLock() { for(; lockCount-->0; Monitor.Exit(yourLockObject)) ; } void GetLocked() { Monitor.Enter(yourLockObject); ++lockCount; } void YourParallelMethod(int x) { GetLocked(); Debug.Print("lockCount={0}", lockCount); } public static void PeformTest() { new Thread( () => { var threadCurrent=Thread.CurrentThread; Debug.Print("ThreadId {0} starting...", threadCurrent.ManagedThreadId); var intanceOfYourClass=new YourClass(); // Parallel.ForEach(Infinity.Enumerable, intanceOfYourClass.YourParallelMethod); foreach(var i in Enumerable.Range(0, 123)) intanceOfYourClass.YourParallelMethod(i); intanceOfYourClass.ReleaseLock(); Monitor.Exit(intanceOfYourClass.yourLockObject); // here SynchronizationLockException thrown Debug.Print("ThreadId {0} finished. ", threadCurrent.ManagedThreadId); } ).Start(); } object yourLockObject=new object(); int lockCount; } 

If you call YourClass.PeformTest() and get a lockCount greater than 1, you re-entered; will not necessarily be simultaneous .
If it is unsafe to re-enter, you are stuck in a foreach loop.
In the code block where Monitor.Exit(intanceOfYourClass.yourLockObject) throws you a SynchronizationLockException , this is because we are trying to Exit more than the time it entered. If you intend to use the lock keyword, you may not encounter this situation other than directly or indirectly, recursive calls. I assume the lock keyword was suggested: it prevents Monitor.Exit from Monitor.Exit .
I noticed a call to Parallel.ForEach , if you're interested, you can check it out for fun.

To test the code, .Net Framework 4.0 is the smallest requirement, and additional namespaces are also required:

 using System.Threading.Tasks; using System.Diagnostics; using System.Threading; using System.Collections; 

Enjoy.

0
source share

All Articles