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
- Always release: Use try/finally
- Set timeouts: Prevent indefinite waits
- Monitor waits: Track queue depth
- Choose right count: Match resource limits
- 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.