Clément
Clément

Reputation: 12937

Release a lock before waiting, and re-acquire it after

In Java, you can associate multiple Condition objects to a single ReentrantLock. What would the C# equivalent be?

Real-world example: The example implementation in the Java Condition documentation uses two Condition objects, notFull and notEmpty, tied to the same lock. How could that example be translated to C#?

Background: I often find Java code using two Condition objects to signal various states, associated to the same Lock; in C#, it seems that you can either

Note: I can think of one way: using Monitor.WaitOne/Monitor.PulseAll on a single object, and checking for the condition after waking up; that's what you do in Java as well to protect against spurious wake-ups. It doesn't really do, though, because it forces you to call PulseAll instead of Pulse, since Pulse might wake up a thread waiting on another condition. Unfortunately, using PulseAll instead of Pulse has performance implications (threads competing for the same lock).

Upvotes: 2

Views: 915

Answers (3)

Dustin Kingen
Dustin Kingen

Reputation: 21245

I haven't come across much C# code that would want to share state within a lock. Without rolling your own you could use a SemaphoreSlim (but I recommend ConcurrentQueue(T) or BlockingCollection(T)).

public class BoundedBuffer<T>
{
    private readonly SemaphoreSlim _locker = new SemaphoreSlim(1,1);
    private readonly int _maxCount = 1000;
    private readonly Queue<T> _items;

    public int Count { get { return _items.Count; } }

    public BoundedBuffer()
    {
        _items = new Queue<T>(_maxCount);
    }

    public BoundedBuffer(int maxCount)
    {
        _maxCount = maxCount;
        _items = new Queue<T>(_maxCount);
    }

    public void Put(T item, CancellationToken token)
    {
        _locker.Wait(token);

        try
        {
            while(_maxCount == _items.Count)
            {
                _locker.Release();
                Thread.SpinWait(1000);
                _locker.Wait(token);
            }

            _items.Enqueue(item);
        }
        catch(OperationCanceledException)
        {
            try
            {
                _locker.Release();
            }
            catch(SemaphoreFullException) { }

            throw;
        }
        finally
        {
            if(!token.IsCancellationRequested)
            {
                _locker.Release();
            }
        }
    }

    public T Take(CancellationToken token)
    {
        _locker.Wait(token);

        try
        {
            while(0 == _items.Count)
            {
                _locker.Release();
                Thread.SpinWait(1000);
                _locker.Wait(token);
            }

            return _items.Dequeue();
        }
        catch(OperationCanceledException)
        {
            try
            {
                _locker.Release();
            }
            catch(SemaphoreFullException) { }

            throw;
        }
        finally
        {
            if(!token.IsCancellationRequested)
            {
                _locker.Release();
            }
        }
    }
}

Upvotes: 0

Reda
Reda

Reputation: 2289

@Jason If the queue is full and you wake only ONE thread, you are not guaranteed that thread is a consumer. It might be a producer and you get stuck.

Upvotes: 0

Jason
Jason

Reputation: 3960

I think if you are doing new development and can do .NET 4 or above, you'll be better served by the new concurrent collection classes, like ConcurrentQueue.

But if you can't make that move, and to strictly answer your question, in .NET this is somewhat simplified imho, to implement a prod/cons pattern you would just do wait and then pulse like below (note that I typed this on notepad)

// max is 1000 items in queue
private int _count = 1000;

private Queue<string> _myQueue = new Queue<string>();

private static object _door = new object();

public void AddItem(string someItem)
{
    lock (_door)
    {
        while (_myQueue.Count == _count)
        {
            // reached max item, let's wait 'till there is room
            Monitor.Wait(_door);
        }

        _myQueue.Enqueue(someItem);
        // signal so if there are therads waiting for items to be inserted are waken up
        // one at a time, so they don't try to dequeue items that are not there
        Monitor.Pulse(_door);
    }
}

public string RemoveItem()
{
    string item = null;

    lock (_door)
    {
        while (_myQueue.Count == 0)
        {
            // no items in queue, wait 'till there are items
            Monitor.Wait(_door);
        }

        item = _myQueue.Dequeue();
        // signal we've taken something out
        // so if there are threads waiting, will be waken up one at a time so we don't overfill our queue
        Monitor.Pulse(_door);
    }

    return item;
}

Update: To clear up any confusion, note that Monitor.Wait releases a lock, therefore you won't get a deadlock

Upvotes: 1

Related Questions