Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you signal and wait atomically with C# thread synchronization?

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!

like image 578
hookeslaw Avatar asked Dec 17 '22 13:12

hookeslaw


2 Answers

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
like image 69
Eric Lippert Avatar answered Jan 27 '23 06:01

Eric Lippert


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);
    }
  }
}
like image 36
Stephen Cleary Avatar answered Jan 27 '23 07:01

Stephen Cleary