Isaac.

csharp

C# Semaphore for Concurrency Control

Control concurrent access to resources with Semaphore.

By Emem IsaacMarch 21, 20222 min read
#csharp#concurrency#semaphore#threading#synchronization
Share:

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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours