2

I have a multi thread application.

One thread inserts in a queue and many thread reads form this queue. In order to read properly, reader threads lock the queue like the following code.

My question is: Does the inserter thread become blocked when the following code is called by reader threads since it uses the same queue? Or it continues inserting without interruption?

lock ( MsgQueue ) {
    if ( MsgQueue.Count == 0 ) {
         Monitor.Wait( MsgQueue );
         continue;
    }
    msg = MsgQueue.Dequeue( );
}
razlebe
  • 7,134
  • 6
  • 42
  • 57
Ahmet Altun
  • 3,910
  • 9
  • 39
  • 64
  • To verify what you said in the comments, can you post a short piece about how you Enqueue ? – H H Jun 17 '11 at 12:49
  • If you are using .Net 4.0 you could just use `ConcurrentQueue` here, see http://msdn.microsoft.com/en-us/library/dd267265.aspx – Steve Townsend Jun 17 '11 at 15:20
  • I advice you to either use [BlockingCollection](http://msdn.microsoft.com/en-us/library/dd267312.aspx) if C# 4.0 or Queue with [AutoRestEvent](http://msdn.microsoft.com/en-us/library/system.threading.autoresetevent.aspx) Check [this at stacoverflow](http://stackoverflow.com/questions/6397566/multithreaded-net-queue-problems-c/6404581#6404581) – Jalal Said Jun 20 '11 at 04:07

5 Answers5

2

The other thread will be blocked by the lock (MsgQueue) while this thread is in the lock but not when in the Monitor.Wait (which releases the lock so other threads can Pulse).

This is the conditional variable pattern: hold the lock while working on the shared state (the queue instance), but release it while waiting for the condition to change (the Monitor.Wait).

Update: based on comment:

No it inserts simply. There is no lock for inserter

Then the queue object is likely to be corrupted. Unless the queue type you are using is inherently thread-safe you must use the same lock for all operations.

Update #2: If this queue is primarily being used to transfer objects from one set of (source) threads to another set of (worker) threads (where each set might just be one) then you should consider a ConcurrentQueue which is thread safe (albeit you will need something like an event to signal there is something on the queue to avoid workers polling).

Richard
  • 106,783
  • 21
  • 203
  • 265
2

Yes, the producer (or inserter) will be blocked while the lock is held by the consumer. Note that the lock is released by a call to Monitor.Wait and then reacquired when control flow has returned back to the caller. All of this assumes your producer attempts to acquire the same lock.

As a side note, the way you have the consumer coded is slightly less efficient than it could be. Because you have a continue statement I have to assume that a while loop wraps the lock which probably makes your code look more like the following.

object msg = null;
while (msg == null)
{
  lock (MsgQueue) 
  {
    if (MsgQueue.Count == 0) 
    {
      Monitor.Wait(MsgQueue);
      continue;
    }
    msg = MsgQueue.Dequeue();
  }
}

This could be refactored so that the wait condition is rechecked inside the lock block. This way you do not have to release and reacquire the lock to perform the check.

object msg = null;
lock (MsgQueue) 
{
  while (MsgQueue.Count == 0) 
  {
    Monitor.Wait(MsgQueue);
  }
  msg = MsgQueue.Dequeue();
}

Again, because I see the continue statement I am assuming you are aware that the wait condition must always be rechecked after a Wait. But, just in case you are not aware of this requirement I will state it here because it is important.

If the wait condition is not rechecked and there is 2 or more consumers then one of them could get inside the lock and dequeue the last item. This could still happen even if the other consumer were moved from the waiting queue to the ready queue via a call to Pulse or PulseAll, but it did not get a chance to reacquire the lock before the first consumer. Obviously, without the recheck a consumer could attempt to operate on an empty queue. It does not matter whether Pulse or PulseAll is used on the producing side. There is still a problem because the Monitor does not give preference to a Wait above an Enter.

Update:

I forgot to point out that if you are using .NET 4.0 then you can take advantage of BlockingCollection which is an implementation of a blocking queue. It is safe for multiple producers and consumers and does all of the blocking for you if the queue is empty.

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

If the inserter thread calls lock ( MsgQueue ) then obviously it will block whenever one of the readers has locked the queue

Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
1

The inserter thread is being blocked at points, yes.

    lock ( MsgQueue ) {
        if ( MsgQueue.Count == 0 ) {  // LINE 1
            Monitor.Wait( MsgQueue ); // LINE 2
            continue;
        }
        msg = MsgQueue.Dequeue( ); // LINE 3
    }

At line 1 the lock is held by the reader, so the inserter is blocked.

At line 2 the lock is released, and not reacquired until the inserter presumably calls Monintor.Pulse on MsgQueue.

At line 3 the lock is still being held (from line 1), and afterwards it is released again due to exiting the lock scope.

Jon
  • 428,835
  • 81
  • 738
  • 806
1

No. I think your questuon is about the meaning of lock ( MsgQueue ) and the metaphor can be a bit misleading. Locking on an object does not change the state of that object in any way, nor does it block other threads, unless those threads use lock on the same object too.

That's why you often see this (better) pattern:

private Queue<MyClass> _queue = ...;
private object _queueLock = new object();
...
lock(_queueLock )
{
    _queue.Enqueue(item);
}

The reference used in the lock only serves as a 'ticket'.

H H
  • 263,252
  • 30
  • 330
  • 514