Tidbits

Semaphore implementation with adjustable concurrency limit

A Semaphore can be used to block threads from continuing execution while another thread uses specific code or resources. But unlike a lock or Monitor, they can allow more than one thread access at a time.
For that purpose the existing implementation receives a limit when initializing, which represents the maximum amount of threads that may “enter”, before any thread has to wait. This limit cannot be changed afterwards anymore. A feature I recently needed.
My solution was my own Semaphore class, that mimics the behavior, but without worrying about the details about how to safely and efficiently block a thread until the counter ticks down.

/// <summary>
/// A simple alternative to <see cref="Semaphore"/> that allows for changes to the thread limit
/// </summary>
public class VariableLimitSemaphore : IDisposable
{
    private readonly EventWaitHandle _waitHandle;
    private readonly object _entryLock;
    private readonly object _counterLock;
    private int _limit;
    private int _counter;
 
    /// <summary>
    /// The current amount of threads that have been granted entry
    /// </summary>
    public int CurrentCounter
    {
        get
        {
            lock (_counterLock)
                return _counter;
        }
    }
 
    /// <summary>
    /// The maximum number of threads allowed entry
    /// </summary>
    public int Limit
    {
        get
        {
            lock (_counterLock)
                return _limit;
        }
        set
        {
            if (value < 1)
                throw new ArgumentOutOfRangeException(nameof(value));
 
            lock (_counterLock)
            {
                _limit = value;
                if (_limit <= _counter)
                    _waitHandle.Reset();
            }
        }
    }
 
    /// <summary>
    /// Creates a new <see cref="VariableLimitSemaphore"/>
    /// </summary>
    /// <param name="initialLimit">The initial limit for concurrent threads</param>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="initialLimit"/> is less than 1</exception>
    public VariableLimitSemaphore(int initialLimit)
    {
        if (initialLimit < 1)
            throw new ArgumentOutOfRangeException(nameof(initialLimit));
 
        _limit = initialLimit;
        _counter = 0;
 
        _waitHandle = new EventWaitHandle(trueEventResetMode.AutoReset);
        _entryLock = new object();
        _counterLock = new object();
    }
 
    /// <summary>
    /// Blocks the current thread until entry is permitted
    /// </summary>
    public void Wait()
    {
        lock (_entryLock)
        {
            _waitHandle.WaitOne();
            lock (_counterLock)
            {
                if (++_counter < _limit)
                    _waitHandle.Set();
            }
        }
    }
 
    /// <summary>
    /// Frees up a single entry for use by another (waiting) thread
    /// </summary>
    public void Release()
    {
        lock (_counterLock)
        {
            if (--_counter < _limit)
                _waitHandle.Set();
        }
    }
 
    /// <inheritdoc />
    public void Dispose()
    {
        _waitHandle?.Dispose();
    }
}

Core of this is an EventWaitHandle, which is used to let threads pass in a controlled manner, one by one. Each time a thread “enters” it increments the counter, compares it to the limit, and lets another thread enter if the limit allows it.
Similarly, when a thread “leaves”, the counter is decremented, compared with the limit and another thread might be granted access.
The only tricky part was eliminating race conditions. For example, if I didn’t use _entryLock to queue up threads even before the WaitHandle, threads could enter, pause before incrementing the counter and that way make another thread that is leaving believe, that there is extra space for yet another thread.

Things you might want to change are overloads for Wait, to include cancellation or timeouts.
Also, this implementation starts accepting threads as soon as it’s initialized, but could easily be extended for more options.
Furthermore, if you have a look at the System.Threading.Semphore source code, you’ll notice a lot of work to make sure that thing runs reliably. A lot of consideration that I didn’t put into this. This is enough for my simple use cases, but I wouldn’t trust it with critical code!