14

I've use the below code to implement and test a blocking queue. I test the queue by starting up 5 concurrent threads (the removers) to pull items off the queue, blocking if the queue is empty and 1 concurrent thread (the adder) to add items to the queue intermitently. However, if I leave it running for long enough I get an exception because one of the remover threads comes out of a waiting state even when the queue is empty.

Does anyone know why I get the exception? Note, I'm interested in knowing why this doesn't work as opposed to a working solution (as I can just Google that).

I'd greatly appreciate your help.

using System;
using System.Threading;
using System.Collections.Generic;

namespace Code
{
    class Queue<T>
    {
        private List<T> q = new List<T>();

        public void Add(T item)
        {
            lock (q)
            {
                q.Add(item);
                if (q.Count == 1)
                {
                    Monitor.Pulse(q);
                }
            }
        }

        public T Remove()
        {
            lock (q)
            {
                if (q.Count == 0)
                {
                    Monitor.Wait(q);
                }
                T item = q[q.Count - 1];
                q.RemoveAt(q.Count - 1);
                return item;
            }
        }
    }

    class Program
    {
        static Random r = new Random();
        static Queue<int> q = new Queue<int>();
        static int count = 1;
        static void Adder()
        {
            while (true)
            {
                Thread.Sleep(1000 * ((r.Next() % 5) + 1));
                Console.WriteLine("Will try to add");
                q.Add(count++);
            }
        }

        static void Remover()
        {
            while (true)
            {
                Thread.Sleep(1000 * ((r.Next() % 5) + 1));
                Console.WriteLine("Will try to remove");
                int item = q.Remove();
                Console.WriteLine("Removed " + item);
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Test");

            for (int i = 0; i < 5; i++)
            {
                Thread remover = new Thread(Remover);
                remover.Start();
            }

            Thread adder = new Thread(Adder);
            adder.Start();
        }
    }
}
Darko
  • 38,310
  • 15
  • 80
  • 107
user1229458
  • 253
  • 1
  • 5
  • 10
  • 1
    Try changing `if (q.Count == 0)` to `while (q.Count == 0)` – K Mehta Feb 23 '12 at 22:28
  • @CodeInChaos: I think it is intended as an exercise. – Eric Lippert Feb 23 '12 at 22:56
  • 4
    Not related to your threading problem: my guess is that you are unaware that there is a much more pleasant way to get your random delay. `Thread.Sleep(r.Next(1000, 5000))` gives you a random number of milliseconds greater than or equal to 1000 and less than 5000. However, you should also be aware that **Random is not thread safe**. You are sharing one instance of Random across many threads and that is *not safe*. – Eric Lippert Feb 23 '12 at 22:59
  • If you do need a thread-safe random number generator, try Jon Skeet's [ThreadLocalRandom](http://msmvps.com/blogs/jon_skeet/archive/2009/11/04/revisiting-randomness.aspx) class. – Brian Feb 24 '12 at 14:11
  • 1
    why this code is required here :if (q.Count == 1)? ,simply call the pulse. – Sudhir.net Dec 31 '15 at 10:19
  • This reminds me of the message loop handling for a Windows GUI application, where it uses `while (GetMessage())` instead of `if(GetMessage())`. – smwikipedia Sep 20 '17 at 02:07

3 Answers3

19

if I leave it running for long enough I get an exception because one of the remover threads comes out of a waiting state even when the queue is empty. Does anyone know why I get the exception?

The question is odd, because obviously you know the answer: your first sentence answers the question asked by the second sentence. You get the exception because a remover thread comes out of the wait state when the queue is empty.

To solve the problem you'll want to use a loop instead of an "if". The correct code is:

while(q.Count == 0) Monitor.Wait(q);

not

if(q.Count == 0) Monitor.Wait(q);

UPDATE:

A commenter points out that perhaps your question was intended to be "under what circumstances can a consumer thread obtain the monitor when the queue is empty?"

Well, you are in a better position to answer that than we are, since you're the one running the program and looking at the output. But just off the top of my head, here's a way that could happen:

  • Consumer Thread 1: waiting
  • Consumer Thread 2: ready
  • Producer Thread 3: owns the monitor
  • There is one element in the queue.
  • Thread 3 pulses.
  • Thread 1 goes to ready state.
  • Thread 3 abandons the monitor.
  • Thread 2 enters the monitor.
  • Thread 2 consumes the item in the queue
  • Thread 2 abandons the monitor.
  • Thread 1 enters the monitor.

And now thread 1 is in the monitor with an empty queue.

Generally speaking when reasoning about these sorts of problems you should think of "Pulse" as being like a pigeon with a note attached to it. Once released it has no connection to the sender, and if it cannot find its home, it dies in the wilderness with its message undelivered. All you know when you Pulse is that if there is any thread waiting then one thread will move to the ready state at some time in the future; you don't know anything else about the relative timing of operations on threads.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • wouldn't `lock(q){ Monitor.Wait(q);}` cause a dead lock? – GETah Feb 23 '12 at 22:32
  • but the question is, how does it get pulsed so that when it comes out of the wait when there is nothing in the Queue? – Keith Nicholas Feb 23 '12 at 22:32
  • @GETah: I am not following you in the slightest. Why would that deadlock? – Eric Lippert Feb 23 '12 at 22:34
  • 2
    @Getah no, Monitor.Wait releases the lock and puts it into a waiting, which happens when a pulse happens – Keith Nicholas Feb 23 '12 at 22:35
  • 4
    @GETah `Monitor.Wait()` doesn't mean "wait for this lock", it means "I want to yield the monitor I currently own until such time as another thread pulses to let me know that I can continue." You are in fact *required* to own the monitor of the object on which you wait. – dlev Feb 23 '12 at 22:36
  • Sorry my bad, not a deadlock but resource starvation. Does the Monitor.Wait(q) release the lock acquired on q right before? – GETah Feb 23 '12 at 22:40
  • 1
    @GETah That's correct: the current owner of the monitor will release the lock and block (at least) until it is pulsed. – dlev Feb 23 '12 at 22:41
  • I'll sleep with a new info tonight! Thanks a lot and sorry for hijacking this thread :) – GETah Feb 23 '12 at 22:43
  • @dlev: Just to clarify: it will block until it is pulsed *and the lock can be re-acquired*. – Eric Lippert Feb 23 '12 at 23:01
  • @EricLippert Right; the pulse (if received) moves the thread from the waiting queue to the ready queue, where it is now *permitted*, though not necessarily guaranteed, to acquire the monitor again. My "at least" parenthetical was a nod to that fact, though I suppose it's not obvious without prior knowledge. – dlev Feb 23 '12 at 23:02
  • Eric - I still have doubts. If T2 starts here as ready it means it has been pulsed which means a producer has just put 1 item in queue. T3 producer was lucky to lock before T2 consumer but when it finishes there will be 2 items and both consumers should have something to consume... – Maciej Feb 24 '12 at 00:09
  • @MaciejDopieralski: Since that is not what is observed, your analysis must be incorrect. And in fact the second sentence of your comment is totally incorrect. It is entirely possible for T2 to be ready without having ever been pulsed. **T2 might have never entered the lock in the first place and therefore never have been put on the wait queue.** – Eric Lippert Feb 24 '12 at 14:53
  • What is observed is one and how it happens is another. But yes, I see it now. It was not obvious from your scenario. – Maciej Feb 24 '12 at 16:03
  • @EricLippert, i want to understand here how "Consumer Thread 2: ready" thread2 ois in ready state, it means some threads send the signal and Thread2 moved into waiting state , at same time it should remove the item from queue, why it is removing when Thread3 send signal? – Sudhir.net Dec 31 '15 at 10:39
  • @EricLippert If there's only a single pending queue. Maybe this issue will never happen. A thread either is blocked or in the pending queue. I post my thoughts here: https://stackoverflow.com/questions/46295828/why-does-monitor-class-keep-2-queues-ready-and-waiting – smwikipedia Sep 19 '17 at 08:52
3

Your code would work if there was 1 consumer but when there are more, this mechanism fails and it should be while(q.Count == 0) Monitor.Wait(q)

The following scenario shows when if(q.Count == 0) Monitor.Wait(q) would fail (it's different than Eric's):

  • consumer 1 is waiting
  • producer has put in an item and is pulsing
  • consumer 1 is ready
  • producer is releasing lock
  • consumer 2 just entered Remove, is lucky and acquires lock
  • consumer 2 sees 1 item, does not wait and takes item out
  • consumer 2 releases lock
  • consumer 1 re-acquires lock but queue is empty

This happens exactly as documentation says it can happen:

When the thread that invoked Pulse releases the lock, the next thread in the ready queue (which is not necessarily the thread that was pulsed) acquires the lock.

Maciej
  • 7,871
  • 1
  • 31
  • 36
1

Eric is of course right; the fact is that while the code appears to cover all the bases; the fact that an exception occurs shows that you haven't.

The race condition is that between the Monitor.Wait on a remover and a Monitor.Pulse on the adder (which releases the lock; but doesn't necessarily immediately trigger a thread waiting to wake up and reacquire it); a subsequent remove thread can acquire the lock and immediately jump the

if (q.Count == 0) 
{ 
  Monitor.Wait(q); 
} 

Statement and go straight to removing the item. Then, the Pulsed thread wakes up and assumes there's an item still there; but there isn't.

The way to fix it, whatever the way the race condition is actually manifesting, is as Eric has said.

Equally if you read the example on Monitor.Pulse you'll see a similar setup to what you have done here but a subtlely different way of doing it.

Andras Zoltan
  • 41,961
  • 13
  • 104
  • 160