Dimkin
Dimkin

Reputation: 690

Using Monitor.Enter to lock variable incremetation

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

Upvotes: 4

Views: 570

Answers (4)

oleksii
oleksii

Reputation: 35905

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
...

Upvotes: 3

Brian Gideon
Brian Gideon

Reputation: 48949

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.

Upvotes: 2

bohdan_trotsenko
bohdan_trotsenko

Reputation: 5357

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.

Upvotes: 2

Daniel James Bryars
Daniel James Bryars

Reputation: 4621

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?

Upvotes: 3

Related Questions