Skip to content

NTDLS.Semaphore Usage

Critical resources and semaphores for C# with pessimistic and optimistic locking, timeouts and deadlock-prevention patterns.

Published Jul 25, 2026

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

An unhandled error has occurred. Reload 🗙