Compze.Threading 0.9.0-alpha

This is a prerelease version of Compze.Threading.
dotnet add package Compze.Threading --version 0.9.0-alpha
                    
NuGet\Install-Package Compze.Threading -Version 0.9.0-alpha
                    
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="Compze.Threading" Version="0.9.0-alpha" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Compze.Threading" Version="0.9.0-alpha" />
                    
Directory.Packages.props
<PackageReference Include="Compze.Threading" />
                    
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 Compze.Threading --version 0.9.0-alpha
                    
#r "nuget: Compze.Threading, 0.9.0-alpha"
                    
#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 Compze.Threading@0.9.0-alpha
                    
#: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=Compze.Threading&version=0.9.0-alpha&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Compze.Threading&version=0.9.0-alpha&prerelease
                    
Install as a Cake Tool

Compze.Threading

Pit of success threading primitives. Impossible to forget to lock. Automatically resolves and diagnoses deadlocks, including both the involved stack traces in the exception.

The Problem

With the tools built into the BCL:

  • Nothing stops you from accessing shared state without locking.
  • Forget once and you have an intermittent bug apt to be very hard to diagnose.
  • Deadlocks hang forever bringing production software to a permantent halt.

Usage requires constant vigilance. Getting it disastrously wrong is all too easy.

Our Solutions

Automatic Deadlock Resolution with Dual Stack Traces

Every lock acquisition in all the abstractions shown below use a LockTimeout internally when acquiring locks (default 2 minutes). When deadlocks occur the timeout elapses and we capture both stack traces in the exception thrown: the thread that was blocked and the thread that was blocking it. No more guessing which thread held the lock.

💡 If the "winning" thread does not promptly (within 10 seconds) release the lock after the deadlock is resolved its stacktrace will not occur in the exception.

IThreadShared<T> Make Forgetting to Lock Impossible.

class MyThreadSafeClass
{
    readonly IThreadShared<PrivateImplementation> _inner = IThreadShared.New(new PrivateImplementation());

    void Add(string key, int value) => _inner.Locked(it => it.Add(key, value));
    int Get(string key) => _inner.Locked(it => it.Get(key));

    class PrivateImplementation{/**/}
}

💡 Note: There is no way to access the PrivateImplementation instance without acquiring the lock. This resolves whole categories of bugs in one fell swoop.

IAwaitableThreadShared<T> — Condition Waits with Read/Update Semantics

To be able to efficiently wait for shared state to reach a certain condition you need IAwaitableThreadShared instead:

class WorkTracker
{
    readonly IAwaitableThreadShared<PrivateImplementation> _inner = IAwaitableThreadShared.New(new PrivateImplementation());

    void Add(WorkItem item) => _inner.Update(it => it.Add(item));

    void Dispose()
    {
        _inner.UpdateWhen(it => it.IsEmpty, it => it.Dispose());
    }

    class PrivateImplementation{/**/}
}

💡 Note: Always use Update if you change state or are at all unsure. Waiting for conditions will never react to updates made inside Read

API Overview

Simple Locking — IThreadShared<T>

class MyService
{
    readonly IThreadShared<MyState> _state = IThreadShared.New(new MyState());

    public int GetValue() => _state.Locked(state => state.Value);
    public void SetValue(int value) => _state.Locked(state => state.Value = value);
}

One operation: Locked. The shared state is the lambda parameter. No way to misuse it.

Read/Update/Wait — IAwaitableThreadShared<T>

class WorkTracker
{
    readonly IAwaitableThreadShared<MyState> _state = IAwaitableThreadShared.WithDefaultTimeouts(new MyState());
    // or: IAwaitableThreadShared.New(new MyState(), LockTimeout.Seconds(5), WaitTimeout.Seconds(30));

    // Read under lock. Does NOT wake waiting threads.
    public int GetValue() => _state.Read(state => state.Value);

    // Update under lock and notify all waiting threads to re-evaluate their conditions
    public void SetValue(int value) => _state.Update(state => state.Value = value);  
    

    // Wait for condition to become true, then read while still holding the lock used for checking the condition
    public int AwaitReady() => _state.ReadWhen(state => state.IsReady, state => state.Value);

    // Wait for condition to become true, then update while still holding the lock used for checking the condition, then notify all waiting threads about the update before releasing the lock.
    public void ProcessWhenReady() => _state.UpdateWhen(state => state.HasWork, state => state.Process()); 

    // Wait for condition to become true
    public void AwaitDone() => _state.Await(state => state.IsDone);
}

Lower-Level — IMonitor and IAwaitableMonitor

CRITICAL: The *When methods (TakeReadLockWhen, TakeUpdateLockWhen, ReadWhen, UpdateWhen, Await, etc.) release the lock while waiting for a condition, just like Monitor.Wait. When the condition is met, the lock is reacquired before the method returns. This means that if you hold the lock and call a waiting method, other threads can acquire the lock during the wait. All IAwaitableCriticalSection implementations — including IAwaitableMonitor and IAwaitableMutex — share this behavior.

Wrap all logic in existing public methods inside calls to _monitor to quickly migrate existing classes:
class MyThreadSafe
{
    IMonitor _monitor = IMonitor.New();
    
    public void DoStuff() => _monitor.Locked(() => 
    {
        //implementation here
    });
}

class MyAwaitableThreadSafe
{
    IAwaitableMonitor _monitor = IAwaitableMonitor.New();

    public AType ReadAType() => _monitor.Read(() => _aField);
    public void UpdateSomething(AType value) => _monitor.Update(() => _aField = value);
    public AType AwaitValue() => _monitor.ReadWhen(() => _aField != null, () => _aField);

}
Lock the entire method body with a using statement. For very hot path code, where even a lambda allocation might be worth caring about.
class MyThreadSafe
{
    IMonitor _monitor = IMonitor.New();
    
    public void DoStuff()
    {
        using var lock = _monitor.TakeLock();
        //implementation here
    };
}

class MyAwaitableThreadSafe
{
    IAwaitableMonitor _monitor = IAwaitableMonitor.New();

    public AType ReadAType()
    {
        using var lock = _monitor.TakeReadLock();
        return _aField;
    }

    public void UpdateSomething(AType value)
    {
        using var lock = _monitor.TakeUpdateLock();
        _aField = value;
    }
    public AType AwaitValue()
    {
        using var lock = _monitor.TakeReadLockWhen(() => _aField != null);
        return _aField;
    }
}

💡 Take*Lock operations are zero allocation, the disposables returned are reused

Lock only in chosen sections of code: Advanced/rare/risky usage:
class RiskyExample
{
    readonly IAwaitableMonitor _monitor = IAwaitableMonitor.WithDefaultTimeout();

    public void ReadSomething()
    {
        //Unsynchronized code
        using(_monitor.TakeReadLock()) { /* read */ }
        //Unsynchronized code
    }

    public void UpdateSomething()
    {
        //Unsynchronized code
        using(_monitor.TakeUpdateLock()) { /* update, notifies waiters */ }
        //Unsynchronized code
    }

    public void WaitThenRead()
    {
        //Unsynchronized code
        using(_monitor.TakeReadLockWhen(() => ready)) { /* waits for condition, then reads */ }
        //Unsynchronized code
    }
}

💡 WARNING: Think carefully before doing this. Much of the problems with BCL primitives come right back. Profile before you assume that more granular locking is needed for performance. It is far more rare than you may think.

Product Compatible and additional computed target framework versions.
.NET 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.

NuGet packages (21)

Showing the top 5 NuGet packages that depend on Compze.Threading:

Package Downloads
Compze.DependencyInjection

Package Description

Compze.Internals.SystemCE

For Compze internal use only. Do not take a direct dependency on this package.

Compze.Abstractions

Core abstractions for the Compze framework: entity IDs, message type contracts, time sources, serialization interfaces, and type mapping infrastructure.

Compze.Tessaging

Messaging infrastructure for the Compze framework including command, query, and event handling.

Compze.Sql.Common

For Compze internal use only. Do not take a direct dependency on this package

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.0-alpha 308 7/24/2026
0.8.0-alpha 247 7/15/2026
0.7.0-alpha 239 7/5/2026
0.6.0-alpha 105 7/4/2026
0.5.0-alpha 708 6/4/2026
0.3.0-alpha 91 3/4/2026