0

Duplicate question. Answer here ...

Lock aqcuired and further attempts to lock do not block: are C# locks re-entrant?

Basically (copied from link above):

Locks in .NET are reentrant. Only acquisitions from other threads are blocked. When the same thread locks the same object multiple times, it simply increments a counter, and decrements it when released. When the counter hits zero, the lock is actually released for access from other threads.


c# .net 4.8 winforms. One button, one multiline textbox.

Simple app - click on button, it waits 2 seconds (doevents for 2 seconds), it writes out.

I clicked the button 6 times, about a second apart. I am aware the doevents allows more click events to be picked up. I can see that the lock is not waiting before processing is finished for previous click., and that the events are finishing in reverse order.

Q1 - Why is the lock not locking?

Q2 - why are the click events completed in reverse order?

Q3 -What's actually happening here?

using System.Windows.Forms;

namespace WindowsFormsApplication_testlock
{
    public partial class Form1 : Form
    {
        object _objLock;
        int _intIteration;
        System.Collections.Generic.Dictionary<int, DateTime> _lstIterationsTimes;
        public Form1()
        {
            InitializeComponent();


            _objLock = new object();
            _intIteration = 0;
            _lstIterationsTimes = new Dictionary<int, DateTime>();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _intIteration++;
            _lstIterationsTimes.Add(_intIteration, DateTime.Now);
            Process(_intIteration);
        }

        private void Process(int intIteration)
        {
            textBox1.Text += "About to lock - " + intIteration.ToString() + Environment.NewLine;
            lock (_objLock)
            {
                textBox1.Text += "Succeeded to lock - " + intIteration.ToString() + Environment.NewLine;
                

                while ( (DateTime.Now - _lstIterationsTimes[intIteration]).TotalSeconds < 2)
                {
                    Application.DoEvents();
                }
                textBox1.Text += "About to unlock - " + intIteration.ToString() + Environment.NewLine ;
            }

            
        }
    }
}

Output:

About to lock - 1

Succeeded to lock - 1

About to lock - 2

Succeeded to lock - 2

About to lock - 3

Succeeded to lock - 3

About to lock - 4

Succeeded to lock - 4

About to lock - 5

Succeeded to lock - 5

About to lock - 6

Succeeded to lock - 6

About to unlock - 6

About to unlock - 5

About to unlock - 4

About to unlock - 3

About to unlock - 2

About to unlock - 1

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
fangled
  • 21
  • 4
  • Sounds like it's all happening on the same thread, in which case the `lock` isn't blocking. – Wai Ha Lee Dec 08 '21 at 13:00
  • Thanks - I already added that to the top as a duplicate ref! So while here, why are the events in reverse order? Do messages get processed with more recent first? – fangled Dec 08 '21 at 14:49
  • Loosely: each time the thread calls `Process` it enters the lock, you call `Application.DoEvents();` which allows the same thread to call `Process`, entering the lock again. As it's the same thread, there's no blocking. At the end, you're in `DoEvents()` multiple times - but no thread has left the lock. Then the innermost call (6) leaves the lock and exits `Process`, then the next-innermost (5), etc. until (1) leaves. – Wai Ha Lee Dec 08 '21 at 15:47
  • @WaiHaLee Thanks sounds a tiny bit confusing. "No thread has left the lock" -there's only thread. This doesn't really explain the reverse order. – fangled Dec 09 '21 at 16:55
  • Whoops - apologies. That was poorly-worded. Maybe it'd be better if I posted an answer instead. – Wai Ha Lee Dec 09 '21 at 17:16

1 Answers1

0

Q1 - Why is the lock not locking?

When you press button1 the call to button1_Click is always on the same thread, the GUI thread. In C# a thread may enter the same lock multiple times.

Q2 - why are the click events completed in reverse order?

Q3 -What's actually happening here?

See below.


What is happening, and why, can be summarised in the following image (apologies for the appalling quality - I did this in PowerPoint) - to keep things simple, I've limited the number of times that the Process method is called to three:

What's happening is the following - all on the GUI thread:

  • button1_Click is triggered, which increments _intIteration to 1 then calls Process(1).
  • Inside Process(1):
    • The lock is entered immediately as there is no other thread in the lock.
    • Within the while loop, the application is allowed to await input from the user through the repeated calls to DoEvents().
    • At some point during while loop, button1 is clicked again. The DoEvents() call doesn't return until button1_Click returns.
    • button1_Click is triggered, which increments _intIteration to 2 then calls Process(2).
    • Inside Process(2) (still on the GUI thread):
      • The lock is entered immediately as there is no other thread in the lock - this thread has entered the lock once already, but a single thread can re-enter the same lock multiple times.
      • Within the while loop, the application is allowed to await input from the user through the repeated calls to DoEvents().
      • At some point during while loop, button1 is clicked again. The DoEvents() call doesn't return until button1_Click returns.
      • button1_Click is triggered, which increments _intIteration to 3 then calls Process(3).
      • Inside Process(3) (still on the GUI thread):
        • The lock is entered immediately as there is no other thread in the lock - this thread has entered the lock twice already, but a single thread can re-enter the same lock multiple times.
        • Within the while loop, the application is allowed to await input from the user through the repeated calls to DoEvents().
        • No user interation happens, so Process(3) returns after printing "About to unlock - 3"
      • The third level of button1_Click returns as Process(3) returned.
      • DoEvents() (called from Process(2)) returns so the execution returns to Process(2).
    • Process(2) returns after printing "About to unlock - 2"
    • The second level of button1_Click returns as Process(2) returned.
  • DoEvents() (called from Process(1)) returns so the execution returns to Process(1).
  • Process(1) returns after printing "About to unlock - 1"
  • The first level of button1_Click returns as Process(1) returned.

Note that once button1 stops being clicked and the while loops exit, there are multiple stack frames in the DoEvents() method. The methods must exit in a last in, first out order which is why Process(3) exits then Process(2) exits then Process(1) exits, resulting in the

About to unlock - 3

About to unlock - 2

About to unlock - 1

that you are unsure about.


As this all happens on one thread, it might be easier to look at the following code which shows you what's happening:

private static object _objLock = new object();

public static void Main(params string[] arguments)
{
    Recurse(1);
}

public static void Recurse(int iteration)
{
    if ( iteration > 3 )
        return;

    Console.WriteLine($"About to lock - {iteration}");

    lock ( _objLock )
    {
        Console.WriteLine($"Entered lock - {iteration}");

        Recurse(iteration + 1);

        Console.WriteLine($"Releasing lock - {iteration}");
    }
}

which writes this to the console:

About to lock - 1
Entered lock - 1
About to lock - 2
Entered lock - 2
About to lock - 3
Entered lock - 3
Releasing lock - 3
Releasing lock - 2
Releasing lock - 1
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
  • 1
    You've put alot of effort in there! Thanks very much. I think my thoughts are that as there is only one thread, the lock is insignificant, and the lock-related code can be removed. I'm happy with the explanation of the lock not blocking - it only offers value in a multi-thread application. I've now read your stuff about the reversal order, and can understand that now. The key is the doevents. It checks for messages in the message queue; if there are messages, IT IMMEDIATELY STOPS AT THAT POINT WHAT IT CURRENTLY HAPPENING, dequeues the message and actions it. It's the stopping didn't realise – fangled Dec 10 '21 at 11:13
  • @fangled: Yes - that's right: `DoEvents()` is what causes the thread to jump from one method to another before continuing, which is how the lock can be entered multiple times by that thread... *If* you found my answer was helpful, consider [accepting](https://stackoverflow.com/help/someone-answers) it (only if you want). – Wai Ha Lee Dec 10 '21 at 13:56