Search code examples
c#multithreadingproducer-consumer

Waiting on multiple different pulses events


I have been given an application that boils down to being a producer-consumer pattern. Several threads are doing some work and updating a single data set so that several more threads can consume that data and do their own work with it. At the moment, it's not terribly complex, all the consuming threads wait on the data set until one of the producers calls a pulseall.

There is now a desire to have one of the consumer threads consume from two different data sets anytime either of the sets changes. The teams desire to keep refactoring to a minimum and my limited experience with threading has given me some issues finding a clean solution.

The quick and dirty solution was to do the waiting and pulsing on a separate object and have the consumer threads check for changes in their data set before continuing. There does not seem to be a way for one thread to wait on two objects, without replacing the generic threads with a more robust threading tool (thread pools, task, etc) unless I'm failing to google the right thing.


Solution

  • If you are willing to do a little refactoring I would recommend switching from Monitor to one of the EventWaitHandle derived classes.

    Depending on the behavior you want you may want AutoResetEvent, that will more closely act like a Monitor.Entier(obj)/Monitor.Exit(obj)

    private readonly object _lockobj = new Object();
    public void LockResource()
    {
        Monitor.Enter(_lockobj);
    }
    
    public void FreeResource()
    {
        Monitor.Exit(_lockobj);
    }
    
    //Which is the same as
    
    private readonly AutoResetEvent _lockobj = new AutoResetEvent(true);
    public void LockResource()
    {
        _lockobj.WaitOne();
    }
    
    public void FreeResource()
    {
        _lockobj.Set();
    }
    

    or you may want ManualResetEvent will more closely act like Monitor.Wait(obj)/Monitor.PulseAll(obj)

    private readonly object _lockobj = new Object();
    public void LockResource()
    {
        Monitor.Enter(_lockobj);
    }
    
    public bool WaitForResource()
    {
        //requires to be inside of a lock.
        //returns true if it is the lock holder.
        return Monitor.Wait(_lockobj);        
    }
    
    public void SignalAll()
    {
        Monitor.PulseAll(_lockobj);   
    }
    
    // Is very close to
    
    private readonly ManualResetEvent _lockobj = new ManualResetEvent(true);
    public bool LockResource()
    {
        //Returns true if it was able to perform the lock.
        return _lockobj.Reset();
    }
    
    public void WaitForResource()
    {
        //Does not require to be in a lock. 
        //if the _lockobj is in the signaled state this call does not block.
        _lockobj.WaitOne();      
    }
    
    public void SignalAll()
    {
        _lockobj.Set();
    }
    

    1 event can wake up multiple threads, to handle multiple events by one thread you can do

    ManualResetEvent resetEvent0 = ...
    ManualResetEvent resetEvent1 = ...
    
    public int WaitForEvent()
    {
        int i = WaitHandle.WaitAny(new WaitHandle[] {resetEvent0, resetEvent1});
        return i;
    }
    

    and i will be the index of the reset event that had Set() called on it.