4

I'm having some issues with thread synchronization in C#. I have a shared object which gets manipulated by two threads, I've made access to the object mutually exclusive using lock(), but I also want to block each thread depending on the state of the shared object. Specially block thread A when the object is empty, block thread B when the object is full, and have the other thread signal the blocked thread when the object state changes.

I tried doing this with a ManualResetEvent, but have run into a race condition where thread B will detect the object is full, move to WaitOne, and thread A will come in and empty the object (signalling the MRE every access, and block itself once the object is empty) before thread A hits its WaitOne, meaning thread A is waiting for the thread to not be full, even though it isn't.

I figure that if I could call a function like 'SignalAndWaitOne', that would atomically signal before waiting, it would prevent that race condition?

Thanks!

hookeslaw
  • 155
  • 2
  • 8
  • 1
    I am not sure I follow you exactly here, a code sample of the threads would probably be helpful. – pstrjds May 24 '11 at 18:36
  • Joe Duffy showed a simple blocking queue and bounded buffer in an article several years ago: [9 Reusable Parallel Data Structures and Algorithms](http://msdn.microsoft.com/en-us/magazine/cc163427.aspx) – Jim Mischel May 24 '11 at 19:06

2 Answers2

7

A typical way to do this is to use Monitor.Enter, Monitor.Wait and Monitor.Pulse to control access to the shared queue. A sketch:

shared object sync = new object()
shared Queue q = new Queue()

Producer()
    Enter(sync)
    // This blocks until the lock is acquired
    while(true)
        while(q.IsFull)
            Wait(sync)     
            // this releases the lock and blocks the thread 
            // until the lock is acquired again

        // We have the lock and the queue is not full.
        q.Enqueue(something)
        Pulse(sync)
        // This puts the waiting consumer thread to the head of the list of 
        // threads to be woken up when this thread releases the lock

Consumer()
    Enter(sync)
    // This blocks until the lock is acquired
    while(true)
        while(q.IsEmpty)
            Wait(sync)
            // this releases the lock and blocks the thread 
            // until the lock is acquired again

        // We have the lock and the queue is not empty.
        q.Dequeue()
        Pulse(sync)
        // This puts the waiting producer thread to the head of the list of 
        // threads to be woken up when this thread releases the lock
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Ah perfect. The ability to unblock a critical section while waiting is exactly what I was missing. One quick question about C# Monitors. Do I not need to call Monitor.Exit(lock) at the end of the critical section to release the lock, or does Pulse(lock) automatically do that? For some reason my code seems to be working without the Exit (to my surprise). Maybe the lock is releasing when the function finishes or when the thread comes back in and calls Enter(lock) a second time. – hookeslaw May 24 '11 at 19:46
  • Just to make sure I got it right, `Monitor.Wait` does the following: (1) Exit, (2) [wait for someone else to acquire the lock], (3) Enter? – configurator May 24 '11 at 23:12
  • @hookeslaw: You'll note in my sketch that nothing ever leaves the critical section, so there is no "exit". What is typically done in practice is that the "Enter" is actually a "lock" statement, so that way you automatically get the "Exit" put in a "finally" block by the compiler for you. If you do intend for the code to leave the loop then yes there probably should be an Exit in there somewhere. – Eric Lippert May 25 '11 at 14:54
  • 1
    @configurator: Basically, though the details are a bit more complicated. Each lock has a queue of "waiting" threads and a queue of "ready" threads. Waiting on a thread exits the lock (perhaps more than once if the lock was entered multiple times on this thread), puts the thread onto the waiting queue, and tells the first thread on the ready queue to enter the lock. Some day we hope that someone will move the thread that just called "Wait" onto the ready queue, and so on. "Pulse" moves a thread from the wait queue to the ready queue. – Eric Lippert May 25 '11 at 15:01
  • @EricLippert I was wondering... don't you suppose to call Pulse before you call Wait? as far as i understood Pulse is moving waiting thread to ready queue and then when wait accures ready thread will jump in action. in the scenario you mentioned there might be a chance that no ready thread are waiting and then both Consumer and Producer are entering waiting queue with no one to awake them (basic case of 1 consumer and one producer??? Please let me know if i missed something.... – Noam Shaish Feb 17 '12 at 14:28
  • @NoamShaish: I'm not following your train of thought here. If you think you've found a bug in my sketch above, provide more details. Under precisely what circumstances is Wait called *after* Pulse, thereby losing the Pulse? – Eric Lippert Feb 17 '12 at 14:47
  • @EricLippert I see what you mean.... the check IsFull / IsEmpty verify it wont happen, Sorry my bad... – Noam Shaish Feb 17 '12 at 15:48
5

A BlockingCollection is already provided by .NET 4.0.

If you're on an earlier version, then you can use the Monitor class directly.

EDIT: The following code is totally untested, and does not handle maxCount values that are small (<= 2). It also doesn't have any provisions for timeouts or cancellation:

public sealed class BlockingList<T>
{
  private readonly List<T> data;
  private readonly int maxCount;

  public BlockingList(int maxCount)
  {
    this.data = new List<T>();
    this.maxCount = maxCount;
  }

  public void Add(T item)
  {
    lock (data)
    {
      // Wait until the collection is not full.
      while (data.Count == maxCount)
        Monitor.Wait(data);

      // Add our item.
      data.Add(item);

      // If the collection is no longer empty, signal waiting threads.
      if (data.Count == 1)
        Monitor.PulseAll(data);
    }
  }

  public T Remove()
  {
    lock (data)
    {
      // Wait until the collection is not empty.
      while (data.Count == 0)
        Monitor.Wait(data);

      // Remove our item.
      T ret = data.RemoveAt(data.Count - 1);

      // If the collection is no longer full, signal waiting threads.
      if (data.Count == maxCount - 1)
        Monitor.PulseAll(data);
    }
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I'm using an earlier version of .NET. Would you mind going into a bit more detail with how to use Monitors to prevent the race condition described above? I looked at monitors before but couldn't think of how to prevent racing between the condition check and the WaitOne. – hookeslaw May 24 '11 at 18:55
  • Added a code sample. Also note Eric Lippert's answer, which shows the same pattern. – Stephen Cleary May 24 '11 at 19:16