Isaac.

C# Semaphore for Concurrency Control

Control concurrent access to resources with Semaphore.

By EMEPublished: February 20, 2025
csharpconcurrencysemaphorethreadingsynchronization

A Simple Analogy

SemaphoreSlim is like a bouncer with a count. It lets N people in at a time, making others wait their turn.


Why Semaphores?

  • Rate limiting: Limit concurrent operations
  • Resource protection: Prevent overload
  • Backpressure: Queue excess requests
  • Fairness: FIFO queuing
  • Async-friendly: Works with async/await

Basic Usage

// Allow 3 concurrent operations
var semaphore = new SemaphoreSlim(3);

async Task ProcessAsync(int id)
{
    await semaphore.WaitAsync();  // Wait for slot
    try
    {
        Console.WriteLine($"Processing {id}");
        await Task.Delay(1000);
    }
    finally
    {
        semaphore.Release();  // Release slot
    }
}

// Start 10 tasks, only 3 run concurrently
var tasks = Enumerable.Range(1, 10)
    .Select(i => ProcessAsync(i))
    .ToList();

await Task.WhenAll(tasks);

With Timeout

var semaphore = new SemaphoreSlim(2);

async Task TryProcessAsync(int id)
{
    // Wait up to 5 seconds
    if (await semaphore.WaitAsync(TimeSpan.FromSeconds(5)))
    {
        try
        {
            await DoWorkAsync(id);
        }
        finally
        {
            semaphore.Release();
        }
    }
    else
    {
        Console.WriteLine($"Task {id} timed out waiting");
    }
}

Connection Pool

public class DatabasePool
{
    private readonly SemaphoreSlim _semaphore;
    private readonly Queue<DbConnection> _connections;
    
    public DatabasePool(int maxConnections)
    {
        _semaphore = new SemaphoreSlim(maxConnections);
        _connections = new Queue<DbConnection>(maxConnections);
        
        for (int i = 0; i < maxConnections; i++)
        {
            _connections.Enqueue(new DbConnection());
        }
    }
    
    public async Task<DbConnection> AcquireAsync()
    {
        await _semaphore.WaitAsync();
        lock (_connections)
        {
            return _connections.Dequeue();
        }
    }
    
    public void Release(DbConnection connection)
    {
        lock (_connections)
        {
            _connections.Enqueue(connection);
        }
        _semaphore.Release();
    }
}

Rate Limiter

public class RateLimiter
{
    private readonly SemaphoreSlim _semaphore;
    private readonly Timer _refillTimer;
    private readonly int _maxRequests;
    
    public RateLimiter(int maxRequests, TimeSpan window)
    {
        _semaphore = new SemaphoreSlim(maxRequests);
        _maxRequests = maxRequests;
        _refillTimer = new Timer(_ => Refill(), null, window, window);
    }
    
    public async Task WaitAsync()
    {
        await _semaphore.WaitAsync();
    }
    
    private void Refill()
    {
        _semaphore.Dispose();
        _semaphore = new SemaphoreSlim(_maxRequests);
    }
}

Best Practices

  1. Always release: Use try/finally
  2. Set timeouts: Prevent indefinite waits
  3. Monitor waits: Track queue depth
  4. Choose right count: Match resource limits
  5. Consider Mutex: For mutual exclusion

Related Concepts

  • Mutex for exclusive access
  • Monitor class
  • ReaderWriterLockSlim
  • AsyncLock patterns

Summary

SemaphoreSlim controls concurrent access to limited resources. Use it for rate limiting, connection pools, and backpressure handling.