4

in the following code example:

class Program
{
    private static int counter = 0;
    public static object lockRef = new object();

    static void Main(string[] args)
    {
        var th = new Thread(new ThreadStart(() => {
            Thread.Sleep(1000);
            while (true)
            {
                Monitor.Enter(Program.lockRef);
                ++Program.counter;
                Monitor.Exit(Program.lockRef);
            }
        }));
        th.Start();

        while (true)
        {
            Monitor.Enter(Program.lockRef);
            if (Program.counter != 100)
            {
                Console.WriteLine(Program.counter);
            }
            else
            {
                break;
            }
            Monitor.Exit(Program.lockRef);
        }
        Console.Read();
    }
}

Why does the while loop inside Main function does not break even if I use lock with Monitor? If I add Thread.Sleep(1) inside the Thread while everything works as expected and even without Monitor…

Is it just happening too fast that the Monitor class doesn't have enough time to lock?

NOTE: The != operator is intended. I know I can set it to < and solve the problem. What I was trying to achieve is to see it working with Monitor class and not working without it. Unfortunately it doesn't work both ways. Thanks

oleksii
  • 35,458
  • 16
  • 93
  • 163
Dimkin
  • 670
  • 2
  • 9
  • 22
  • 1
    Just to be sure, and I think you may have not used it to show the issue you are trying to re-create, but do you know about [Interlocked.Increment](http://msdn.microsoft.com/en-us/library/dd78zt0c.aspx)? That is the "correct" way to increment a variable in a thread safe way. – Scott Chamberlain Sep 09 '13 at 14:15
  • @ScottChamberlain `Interlocked.Increment` ensures that the read and write happen atomically, that is, that no other thread can execute between the read and write. This is not an issue here as the OP is locking around the incrementation. – Rotem Sep 09 '13 at 14:22
  • @Rotem Which is why I said "*and I think you may have not used it to show the issue you are trying to re-create*". However if he is not aware of it, it is something important to know how to do as a programmer writing multi-threaded applications. – Scott Chamberlain Sep 09 '13 at 14:23
  • @ScottChamberlain Sorry, I misread your comment then. – Rotem Sep 09 '13 at 14:26
  • I do aware of Interlocked.Increment. It did not solve the problem though. Actually the explanation below explained the behavior pretty well. Thanks – Dimkin Sep 09 '13 at 17:22

4 Answers4

3

The first thread with the while, might get scheduled twice in a row (ie the Monitor might not be fair.)

See this related question: Does lock() guarantee acquired in order requested?

Community
  • 1
  • 1
Daniel James Bryars
  • 4,429
  • 3
  • 39
  • 57
3

Let's assume you have 1 CPU available. This is how the execution will look like

T1        [SLEEP][INCREMENT][SLEEP][INCREMENT][SLEEP][INCREMENT][SLEEP]
T2   --[L][CK][UL][L][CK][UL][L][CK][UL][L][CK][UL][L][CK][UL][L][CK][UL]
CPU1 [  T2  ][T1][  T2  ][  T1  ][  T2  ][T1][  T2  ][  T1  ][  T2  ][T1]...

Where:

T1 is th thread
T2 is main thread
[L][CK][UL] is lock, check, unlock - the workload of the main thread
CPU1 is task scheduling for the CPU

Note a short [T1] is a call to Thread.Sleep. This results in the current thread yielding control immediately. This thread will not be scheduled for executing for time greater or equal to the specified milisecond parameter.

Longer [ T1 ] is where increment in while loop happens.

Important: T1 will not execute a single increment and then switch to another thread. This is where the problem. It will do many iterations until the current thread execution quant expires. On average you can think of execution quant ~ 10-30 mili seconds.

This is exactly supported by the output, which on my machine was

0
0
0
...
56283
56283
56283
...
699482
699482
699482
...
oleksii
  • 35,458
  • 16
  • 93
  • 163
2

Because CPU chunk is typically 40ms. During this timeframe the thread manages to do lots of increments. It's not the case that a thread exits a monitor and gets a context switch immediately.

bohdan_trotsenko
  • 5,167
  • 3
  • 43
  • 70
2

The Monitor class (or lock keyword) is used to enter and exit a critical section. A critical section is a block of code that is guaranteed to execute serially relative any other critical section defined by the same object reference (the parameter to Monitor.Enter). In other words, two or more threads executing critical sections defined by the same object reference must do so in such a manner that precludes them from happening simultaneously. There is no guarantee that the threads will do this in any particular order though.

For example, if we label the two critical section blocks of your code A and B the two threads as T1 and T2 then any of the following are valid visualize representations of the execution sequences.

T1: A A A . . . A . A A .
T2: . . . B B B . B . . B

or

T1: . A A . . A A
T2: B . . B B . .

or

T1: A A A A A A A .
T2: . . . . . . . B

or

T1: A . A . A . A . A .
T2: . B . B . B . B . B

The domain of possible interleaving permutations is infinite. I just showed you an infinitesimally small subset. It just so happens that only the last permutation will result in your program working the way you expected. Of course, that permutation is extremely unlikely useless you introduce other mechanisms to force it to happen.

You mentioned that Thread.Sleep(1) changed the behavior of your program. This is because it is influencing how the OS schedules the execution of threads. Thread.Sleep(1) is actually a special case that forces the calling thread to yield its time slice to another thread any processor. It was not clear to me where you put this call in your program so I cannot comment too much on why it delivered the desired behavior. But, I can say that it is mostly accidental.

Also, I have to point out that you have a pretty major bug in this program. When you jump out of the while loop via break you are bypassing the Monitor.Exit call which will leave the lock in an acquired state. It is much better to use the lock keyword because it will wrap the Monitor.Enter and Monitor.Exit into a try-finally block that will guarantee that the lock will always be released.

Brian Gideon
  • 47,849
  • 13
  • 107
  • 150