ConcurrencyC#verifiedVerified
Semaphore Pattern in C#
Control access to a finite pool of resources by maintaining a counter that threads atomically increment (release) and decrement (acquire), blocking when the count reaches zero.
How to Implement the Semaphore Pattern in C#
1Step 1: Use SemaphoreSlim to limit concurrent access
public class ConnectionPool
{
private readonly SemaphoreSlim _semaphore;
private readonly int _maxConnections;
public ConnectionPool(int maxConnections)
{
_maxConnections = maxConnections;
_semaphore = new SemaphoreSlim(maxConnections, maxConnections);
}2Step 2: Acquire a connection (blocks if pool exhausted)
public async Task<string> AcquireAsync()
{
await _semaphore.WaitAsync();
return $"Connection (available: {_semaphore.CurrentCount}/{_maxConnections})";
}3Step 3: Release the connection back to the pool
public void Release()
{
_semaphore.Release();
}
}
// Usage:
// var pool = new ConnectionPool(3);
// var tasks = Enumerable.Range(0, 10).Select(async i =>
// {
// var conn = await pool.AcquireAsync();
// Console.WriteLine($"Task {i}: {conn}");
// await Task.Delay(100);
// pool.Release();
// });
// await Task.WhenAll(tasks);using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
// [step] Generic async resource pool using semaphore
public sealed class ResourcePool<T> : IAsyncDisposable
where T : class
{
private readonly SemaphoreSlim _semaphore;
private readonly ConcurrentBag<T> _available = [];
private readonly Func<Task<T>> _factory;
private readonly Func<T, Task>? _destroyer;
private readonly Func<T, Task<bool>>? _validator;
private readonly ILogger<ResourcePool<T>> _logger;
private readonly int _maxSize;
private int _created;
private bool _disposed;
public ResourcePool(
Func<Task<T>> factory,
int maxSize,
ILogger<ResourcePool<T>> logger,
Func<T, Task>? destroyer = null,
Func<T, Task<bool>>? validator = null)
{
_factory = factory;
_maxSize = maxSize;
_logger = logger;
_destroyer = destroyer;
_validator = validator;
_semaphore = new SemaphoreSlim(maxSize, maxSize);
}
public int Available => _available.Count;
public int InUse => _created - _available.Count;
// [step] Acquire a resource with timeout and validation
public async Task<PooledResource<T>> AcquireAsync(
TimeSpan? timeout = null,
CancellationToken ct = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var acquired = await _semaphore.WaitAsync(
timeout ?? TimeSpan.FromSeconds(30), ct);
if (!acquired)
throw new TimeoutException(
$"Could not acquire resource within timeout " +
$"(pool: {InUse}/{_maxSize} in use)");
T resource;
if (_available.TryTake(out var existing))
{
// Validate existing resource
if (_validator is not null && !await _validator(existing))
{
_logger.LogDebug("Resource failed validation, creating new");
if (_destroyer is not null)
await _destroyer(existing);
Interlocked.Decrement(ref _created);
resource = await CreateResourceAsync();
}
else
{
resource = existing;
}
}
else
{
resource = await CreateResourceAsync();
}
_logger.LogDebug("Acquired resource ({InUse}/{Max} in use)",
InUse, _maxSize);
return new PooledResource<T>(resource, this);
}
// [step] Return resource to pool
internal void Return(T resource)
{
if (_disposed)
{
_ = DestroyResourceAsync(resource);
return;
}
_available.Add(resource);
_semaphore.Release();
_logger.LogDebug("Returned resource ({InUse}/{Max} in use)",
InUse, _maxSize);
}
private async Task<T> CreateResourceAsync()
{
var resource = await _factory();
Interlocked.Increment(ref _created);
return resource;
}
private async Task DestroyResourceAsync(T resource)
{
if (_destroyer is not null)
await _destroyer(resource);
Interlocked.Decrement(ref _created);
}
// [step] Dispose all resources
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
while (_available.TryTake(out var resource))
await DestroyResourceAsync(resource);
_semaphore.Dispose();
_logger.LogInformation("Resource pool disposed");
}
}
// [step] RAII wrapper for automatic resource return
public sealed class PooledResource<T>(
T resource, ResourcePool<T> pool) : IDisposable where T : class
{
private int _disposed;
public T Value => _disposed == 0
? resource
: throw new ObjectDisposedException(nameof(PooledResource<T>));
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
pool.Return(resource);
}
}Semaphore Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Semaphore Pattern in the Real World
“Imagine a car park with exactly three spaces. A ticket machine at the entrance (the semaphore) issues a ticket only if spaces remain, lifting the barrier; arriving drivers with no ticket available must wait. When a car exits, the machine automatically increments its counter and releases the next waiting driver — the car park never exceeds capacity.”