NTDLS.Semaphore 3.6.4

dotnet add package NTDLS.Semaphore --version 3.6.4
                    
NuGet\Install-Package NTDLS.Semaphore -Version 3.6.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NTDLS.Semaphore" Version="3.6.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NTDLS.Semaphore" Version="3.6.4" />
                    
Directory.Packages.props
<PackageReference Include="NTDLS.Semaphore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NTDLS.Semaphore --version 3.6.4
                    
#r "nuget: NTDLS.Semaphore, 3.6.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NTDLS.Semaphore@3.6.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NTDLS.Semaphore&version=3.6.4
                    
Install as a Cake Addin
#tool nuget:?package=NTDLS.Semaphore&version=3.6.4
                    
Install as a Cake Tool

NTDLS.Semaphore

NTDLS.Semaphore is a .NET library (net8.0 / net9.0 / net10.0) that makes it easy to guarantee sequential access to variables and sections of code in multi-threaded applications. Instead of sprinkling lock statements throughout your code, you wrap the data or the critical section once and then execute work against it through a small set of well-named methods: blocking (Use, Read, Write), non-blocking (TryUse, TryRead, TryWrite), timed variants with millisecond timeouts, and multi-lock variants for when several locks must be held at once.

📦 Be sure to check out the NuGet package: https://www.nuget.org/packages/NTDLS.Semaphore

When to use this package

Reach for NTDLS.Semaphore when you need to protect shared state from parallel or non-sequential thread access and want more control than a plain lock statement gives you:

  • You want to guard a variable, not just a block of code. PessimisticCriticalResource<T> and OptimisticCriticalResource<T> encapsulate a value together with its lock, so every access goes through the same protected entry points.
  • You want reads to not block other readers. OptimisticCriticalResource<T> acquires read-only and exclusive locks separately, so concurrent Read operations do not queue behind each other.
  • You want to avoid blocking the caller. The Try* overloads attempt the lock once (or for a bounded number of milliseconds) and report whether they succeeded instead of waiting indefinitely — useful for fire-and-forget updates, UI code, or back-pressure logic.
  • You need to hold several locks at once. The ...All overloads acquire the resource's own lock plus an array of additional ICriticalSection locks atomically, releasing everything cleanly if any one of them cannot be obtained.
  • You want deadlock-aware retries. DeadlockAvoidanceTryRead / DeadlockAvoidanceTryWrite retry a timed lock attempt while a cancellation token stays uncancelled.
  • You are debugging deadlocks or race conditions. Optional process-wide thread ownership tracking records which thread holds which lock.

Usage

Pessimistic critical resource

PessimisticCriticalResource<T> protects a reference-type variable by always taking an exclusive lock before executing a delegate against it. There are nullable and non-nullable return variants of each method so you can return any type from the delegate.

using NTDLS.Semaphore;

public class Car
{
    public string? Name { get; set; }
    public int NumberOfWheels { get; set; }
}

// Wrap the list together with its lock.
public PessimisticCriticalResource<List<Car>> Cars { get; } = new();

// Blocks until the lock is acquired, then adds the car.
public void Add(Car car)
{
    Cars.Use((obj) => obj.Add(car));
}

// Returns the first matching car; UseNullable<T> supports nullable results.
public Car? GetByName(string name)
{
    return Cars.UseNullable((obj) => obj.Where(o => o.Name == name).FirstOrDefault());
}

// TryUse does not block: it returns false if the lock could not be obtained.
public bool TryAdd(Car car)
{
    return Cars.TryUse((obj) => obj.Add(car));
}

// Timed variant: wait up to 500 ms for the lock.
public bool TryAdd(Car car, int timeoutMilliseconds)
{
    return Cars.TryUse(timeoutMilliseconds, (obj) => obj.Add(car));
}

// Peek executes without taking the lock at all (unsafe unless you know no
// other thread can touch the value concurrently).
public void PrintCount()
{
    Cars.Peek((obj) => Console.WriteLine(obj.Count));
}
Locking multiple resources at once

TryUseAll acquires the resource's own lock plus every lock in the supplied array; if any lock cannot be obtained, the ones already held are released and false is returned.

using NTDLS.Semaphore;

public PessimisticCriticalResource<List<Car>> Cars { get; } = new();
public PessimisticSemaphore OtherLock1 { get; } = new();
public PessimisticSemaphore OtherLock2 { get; } = new();

// Only runs if "Cars" AND both other locks can be obtained within the timeout.
public Car? TryGet(string name, int timeoutMilliseconds)
{
    // Nullable-result overload: returns null if any lock could not be obtained.
    return Cars.TryUseAllNullable(
        new ICriticalSection[] { OtherLock1, OtherLock2 },
        timeoutMilliseconds,
        out bool wasLockObtained,
        (obj) => obj.Where(o => o.Name == name).FirstOrDefault());
}

Optimistic critical resource

OptimisticCriticalResource<T> separates read-only and exclusive access so that read operations do not block other read operations. It is up to you to pick the right lock kind per operation. Note: read-only locks only express intention — nothing stops a reader from mutating the value, but doing so will cause race conditions.

using NTDLS.Semaphore;

public class Car
{
    public string? Name { get; set; }
    public int NumberOfWheels { get; set; }
}

public OptimisticCriticalResource<List<Car>> Cars { get; } = new();

// Exclusive access: blocks until no other writer holds the lock.
public void Add(Car car)
{
    Cars.Write((obj) => obj.Add(car));
}

// Read-only access: does not block other readers.
// The delegate SHOULD NOT modify the passed value.
public Car? GetByName(string name)
{
    return Cars.ReadNullable((obj) => obj.Where(o => o.Name == name).FirstOrDefault());
}

// Non-blocking write: returns false instead of waiting if the lock is busy.
public bool TryAdd(Car car)
{
    return Cars.TryWrite((obj) => obj.Add(car));
}

// Timed non-blocking write: wait up to 500 ms for the exclusive lock.
public bool TryAdd(Car car, int timeoutMilliseconds)
{
    return Cars.TryWrite(timeoutMilliseconds, (obj) => obj.Add(car));
}
Reading across multiple resources

ReadAll / TryReadAll take the resource's own read-only lock plus read-only locks on an array of other sections, so several resources can be snapshotted consistently without blocking writers on unrelated resources.

using NTDLS.Semaphore;

public OptimisticCriticalResource<List<Car>> Cars { get; } = new();
public OptimisticCriticalResource<List<string>> GarageNames { get; } = new();

// Acquires read locks on Cars plus every section in the supplied array.
public bool Snapshot(out List<Car>? cars, out List<string>? names)
{
    cars = null;
    names = null;

    // Waits up to 1000 ms; returns false if any read lock could not be obtained.
    bool wasLockObtained = Cars.TryReadAll(
        new ICriticalSection[] { GarageNames },
        1000,
        (obj) =>
        {
            // The delegate only runs once all read locks are held.
            cars = new List<Car>(obj);
            names = new List<string>();
        });

    if (wasLockObtained)
    {
        // Read the second resource under its own read-only lock.
        names = GarageNames.ReadNullable((g) => g.ToList());
    }

    return wasLockObtained;
}

Critical section (semaphore)

When you only need to protect a region of code rather than a specific variable, use PessimisticSemaphore directly.

using NTDLS.Semaphore;

private readonly PessimisticSemaphore _pessimisticSemaphore = new();
private int _value;

public int Value
{
    get
    {
        // Use<T> blocks until the section is free, then returns the delegate result.
        return _pessimisticSemaphore.Use(() => _value);
    }
    set
    {
        _pessimisticSemaphore.Use(() => _value = value);
    }
}

// TryUse does not block: returns true only if the section was free.
public bool TrySetValue(int newValue)
{
    return _pessimisticSemaphore.TryUse(() => _value = newValue);
}

Thread ownership tracking

If you need to see which thread owns each semaphore or critical section, enable process-wide ownership tracking. Once enabled it stays enabled for the life of the process, so treat it as a debugging aid for hunting deadlocks and race conditions.

using NTDLS.Semaphore;

// Enables tracking for the whole process (debugging only).
ThreadLockOwnershipTracking.Enable();

// Afterwards, inspect PessimisticSemaphore.Ownership / OptimisticSemaphore.Ownership
// (Dictionary<int, HeldLock>, keyed by thread id) to see who holds what.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on NTDLS.Semaphore:

Package Downloads
NTDLS.ReliableMessaging

Simple, fast, secure and lightweight event or convention-based RPC implementation.

NTDLS.DelegateThreadPooling

Very high-performance active thread pool where work items can be queued as delegate functions. Allows you to easily enqueue infinite FIFO worker items or enforce queue size, wait on collections of those items to complete, and total control over the pool size. Also allows for multiple pools, so that different workloads do not interfere with one another.

NTDLS.StreamFraming

Stream wrapper (typically TCP/IP/NetworkStream) that enables reliable framing, compression, optional encryption, two-way communication, and support for query/reply. Messages are guaranteed to be received in their entirety and in the order which they were sent.

NTDLS.DatagramMessaging

Set of classes and extensions methods that allow you to send/receive UDP packets with ease. Handling corruption checks, concatenation, fragmentation, serialization, compression and encryption.

NTDLS.Wv.Engine

Warped Vector is a 2D game engine written in C# and rendered with DirectX.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.6.4 63 11/3/2026
3.6.2 864 5/27/2026
3.6.1 191 5/27/2026
3.6.0 146 5/14/2026
3.5.4 1,668 11/13/2025
3.5.3 392 10/27/2025
3.5.2 1,967 1/20/2025
3.5.1 1,366 1/8/2025
3.5.0 670 1/4/2025
3.4.11 405 1/4/2025
3.4.10 344 12/31/2024
3.4.9 1,561 10/22/2024
3.4.8 322 10/22/2024
3.4.7 327 10/22/2024
3.4.6 323 10/22/2024
3.4.5 458 10/13/2024
3.4.4 383 10/13/2024
3.4.3 354 10/13/2024
3.4.2 1,386 8/25/2024
3.4.1 421 8/24/2024
Loading failed

Additional peek overloads.