NTDLS.Semaphore Usage
Critical resources and semaphores for C# with pessimistic and optimistic locking, timeouts and deadlock-prevention patterns.
NTDLS.Semaphore
What it is. A family of classes that protect variables and code sections from parallel thread access. PessimisticCriticalResource<T> always takes an exclusive lock; OptimisticCriticalResource<T> separates read and write locks so reads do not block other reads; plain PessimisticSemaphore guards a bare section of code. All support timeout-based "try" variants with lightweight cancellation, multi-lock acquisition for deadlock prevention, and optional thread ownership tracking for debugging.
When to reach for it. You are writing multithreaded C# and want lock discipline as a first-class object - wrapping a shared collection or property instead of sprinkling lock statements - especially when you need timeouts or coordinated multi-lock acquisition. Skip it for the simple case where lock or System.Threading.SemaphoreSlim is enough; this library pays off in complex concurrency scenarios.
Getting started
Wrap a shared field in a critical resource and execute work inline:
public class Car
{
public string? Name { get; set; }
public int NumberOfWheels { get; set; }
}
public PessimisticCriticalResource<List<Car>> Cars { get; } = new();
public void Add(Car car)
{
Cars.Use((obj) => obj.Add(car));
}
public Car? GetByName(string name)
{
return Cars.Use((obj) => obj.Where(o => o.Name == name).FirstOrDefault());
}
public bool TryAdd(Car car, int timeoutMs)
{
// TryUse reports whether the lock was obtained within the timeout.
Cars.TryUse(out bool wasLockObtained, timeoutMs, (obj) => obj.Add(car));
return wasLockObtained;
}
For read-heavy data, use the optimistic variant so concurrent readers never queue behind each other:
public OptimisticCriticalResource<List<Car>> Cars { get; } = new();
public void Add(Car car) => Cars.Write((obj) => obj.Add(car));
public Car? GetByName(string name) =>
Cars.Read((obj) => obj.Where(o => o.Name == name).FirstOrDefault());
When several locks must be held together, TryUseAll acquires them as a set with a single timeout, which prevents the classic two-resource deadlock. And while chasing a race condition in development, call ThreadLockOwnershipTracking.Enable() once at startup and inspect ThreadLockOwnershipTracking.LockRegistration to see which thread owns every lock.
NuGet: NTDLS.Semaphore