STMSharp brings Software Transactional Memory to .NET: write your concurrent logic as atomic transactions over shared variables, with optimistic snapshots and a lock-free CAS commit that prevents lost updates under contention.
- Transaction-based memory model: manage and update shared variables without explicit locks.
- Atomic transactions: automatic retries with configurable max attempts.
- Conflict detection: optimistic snapshot validation that preserves consistency.
- Configurable backoff strategies:
Exponential,ExponentialWithJitter(default),Linear,Constant. - Read-only transactions: validate snapshots without allowing writes, for safer read-heavy workloads.
- Diagnostics: global conflict/retry counters per
Transaction<T>viaStmDiagnostics.
Software Transactional Memory (STM) is a concurrency control mechanism that simplifies writing concurrent programs by providing an abstraction similar to database transactions. STM allows developers to work with shared memory without the need for explicit locks, reducing the complexity of concurrent programming.
- Transactions: operations on shared variables are grouped into transactions. A transaction is a unit of work that must be executed atomically.
- Atomicity: a transaction is executed as a single, indivisible operation. Either all operations within the transaction are completed, or none are.
- Isolation: transactions are isolated from each other, even when running concurrently.
- Conflict detection: STM tracks changes to shared variables and detects conflicts when multiple transactions try to modify the same variable.
- Composability: STM transactions can be composed and nested, making it easier to build complex operations.
- Simplified concurrency control: no low-level locking, fewer deadlocks and race conditions.
- Scalability: better behaviour than lock-based systems under high contention.
- Composability and modularity: complex operations can be built from smaller transactional pieces.
In STMSharp, STM is implemented using transactions that read from and write to STM variables. Transactions can be automatically retried using a backoff strategy to handle conflicts, making it easier to work with shared data in concurrent environments.
STMVariable<T>stores a value and a monotonic version (long).- A transaction keeps:
_reads(cache, including read-your-own-writes),_writes(buffered updates),_snapshotVersions(immutable version per first observation).
Commit protocol (lock-free):
- Guard: each write must have a captured snapshot.
- Reserve: for each write, CAS the version from
even → odd(based on the immutable snapshot) viaTryAcquireForWrite. - Re-validate: for read-set entries, the current version must still equal the snapshot and be even (not reserved).
- Write & release: apply buffered values and increment the version
odd → even(commit complete).
This ensures serializability and prevents lost updates without runtime locks.
-
STMVariable<T>
Encapsulates a shared value and its version. Supports:- transactional access via
ReadWithVersion()/Version, - direct writes via
Write(T)that are protocol-compatible (they reserveeven → oddand releaseodd → even), see caveats below.
- transactional access via
-
Transaction<T>
Internal transactional context used bySTMEngine. Tracks:- read cache (
_reads), - buffered writes (
_writes), - immutable snapshot versions (
_snapshotVersions), and implements the optimistic commit protocol.
- read cache (
-
STMEngine
Public façade exposingAtomic<T>(...)overloads, with:- configurable retry/backoff,
- support for read-only and read-write modes,
- overloads that accept
StmOptions.
-
StmOptions
Immutable configuration for transactional execution:MaxAttemptsBaseDelay,MaxDelayBackoffTypeTransactionMode(ReadWrite,ReadOnly)
-
StmDiagnostics
Public diagnostics helper:GetConflictCount<T>()GetRetryCount<T>()Reset<T>()
Counters are per closed generic type (Transaction<int> vs Transaction<string>).
STMSharp uses an even/odd version scheme and Compare-And-Exchange (CAS) to coordinate writers:
- Invariants
- Even version ⇒ variable is free (no writer holds a reservation).
- Odd version ⇒ variable is reserved by some writer (during a commit attempt or a direct write that follows the same protocol).
- Transactional commits are the recommended way to mutate shared state under concurrency; direct writes are protocol-compatible but bypass transactional composition and conflict semantics (use with care under contention).
- Reserve (CAS)
// success only if current == snapshotVersion (even) // sets version to snapshotVersion + 1 (odd), meaning "reserved" Interlocked.CompareExchange(ref version, snapshotVersion + 1, snapshotVersion);
- Revalidation
- For each read-set entry:
currentVersion == snapshotVersionand(currentVersion & 1) == 0. - For each write-set entry: already reserved by the current commit; skip.
- For each read-set entry:
- Write & release
- Write the new value, then
Interlocked.Increment(ref version)to turnodd → even(commit complete). - On abort,
ReleaseAfterAbort()also increments once to revertodd → even.
- Write the new value, then
- Deterministic ordering
- All reservations over the write-set are attempted in a stable total order (by a per-variable unique id) to reduce livelock under contention.
- On failure, only the already acquired reservations are released, in reverse order.
- Snapshots
- The first observation of a variable (read or write-first) captures an immutable
(value, version)pair used both for validation and reservation.
- The first observation of a variable (read or write-first) captures an immutable
Basic example
// Initialize a shared STM variable
var sharedVar = new STMVariable<int>(0);
// Perform an atomic transaction to increment the value
await STMEngine.Atomic<int>(tx =>
{
var value = tx.Read(sharedVar);
tx.Write(sharedVar, value + 1);
});
// Perform another atomic transaction
await STMEngine.Atomic<int>(tx =>
{
var value = tx.Read(sharedVar);
tx.Write(sharedVar, value + 1);
});Using StmOptions and read-only mode
var sharedVar = new STMVariable<int>(0);
// Read-only transaction (throws if Write is called)
var readOnlyOptions = StmOptions.ReadOnly;
await STMEngine.Atomic<int>(async tx =>
{
var value = tx.Read(sharedVar);
Console.WriteLine($"Current value: {value}");
// tx.Write(sharedVar, 123); // would throw InvalidOperationException
}, readOnlyOptions);
// Custom retry/backoff policy
var customOptions = new StmOptions(
MaxAttempts: 5,
BaseDelay: TimeSpan.FromMilliseconds(50),
MaxDelay: TimeSpan.FromMilliseconds(1000),
Strategy: BackoffType.ExponentialWithJitter,
Mode: TransactionMode.ReadWrite
);
await STMEngine.Atomic<int>(async tx =>
{
var value = tx.Read(sharedVar);
tx.Write(sharedVar, value + 1);
}, customOptions);Diagnostics
// Reset counters for int-transactions
StmDiagnostics.Reset<int>();
// Run some atomic operations...
var conflicts = StmDiagnostics.GetConflictCount<int>();
var retries = StmDiagnostics.GetRetryCount<int>();
Console.WriteLine($"Conflicts: {conflicts}, Retries: {retries}");Represents a shared STM variable within the system.
| Member | Description |
|---|---|
ReadWithVersion() |
Atomically reads the value and version. |
Write(T value) |
Atomically writes a new value. |
int Version { get; } |
Gets the current version of the variable. |
IncrementVersion() |
Increments the version manually. |
⚠️ Intended for internal STM operations only, not exposed to user code directly.
Concrete implementation of a thread-safe STM variable using Volatile and Interlocked.
| Field/Method | Description |
|---|---|
_boxedValue |
Internal boxed value to support both value and reference types. |
Read() |
Simple thread-safe read. |
Write(T value) |
Writes the new value and increments the version. |
ReadWithVersion() |
Returns a consistent snapshot of value and version. |
Version / IncrementVersion() |
Handles versioning for conflict detection. |
✅ Used as the shared state managed inside transactions.
Represents an atomic unit of work. Implements pessimistic isolation and conflict detection via version locking.
| Field | Description |
|---|---|
_reads |
Cache of read values. |
_writes |
Pending writes to apply at commit time. |
_lockedVersions |
Versions locked during reads to check for conflicts. |
Read(...) |
Reads from STM variable and locks its version. |
Write(...) |
Records an intended write to apply later. |
CheckForConflicts() |
Verifies if any STM variable has changed since read. |
Commit() |
Applies writes if no conflicts are detected. |
ConflictCount / RetryCount |
Static counters for diagnostics. |
♻️ A new transaction is created on each attempt (controlled by
STMEngine).
Coordinates STM execution with retry and exponential backoff strategy.
| Method | Description |
|---|---|
Atomic<T>(Action<Transaction<T>>) |
Runs a synchronous transactional block. |
Atomic<T>(Func<Transaction<T>, Task>) |
Runs an async transactional block. |
DefaultMaxAttempts / DefaultInitialBackoffMilliseconds |
Default retry/backoff configuration. |
🔁 Retries the transaction on conflict, doubling delay after each failure.
Detailed performance measurements were conducted using BenchmarkDotNet to compare variable access and atomic operations under various backoff strategies.
- Scope: Execution time, memory allocations, and GC activity
- Operations: Write/Read (standard), Atomic Write/Read
- Strategies: Exponential, Exponential + Jitter, Linear, Constant
This project includes a benchmarking application designed to test and simulate the behavior of the STMSharp library under varying conditions. The benchmark is built to analyze the efficiency and robustness of the STM mechanism. The benchmark parameters are configurable through a JSON file named appsettings.json. This allows centralized and flexible management of the values used for testing.
The goal of the benchmark is to measure the performance of the STMSharp library based on:
- Number of Threads: The number of concurrent threads accessing the transactional memory.
- Number of Operations: The number of transactions executed by each thread.
- Backoff Time: The delay applied in case of conflicts, with configurable backoff strategies (Exponential, Exponential+Jitter, Linear, Constant).
At the end of execution, the benchmark provides several statistics:
- Total Duration: The total time taken to complete the benchmark.
- Average Time per Operation: Calculated as the ratio between the total duration and the total number of operations.
- Conflicts Resolved: The total number of conflicts handled by the STM system.
- Retries Attempted: The total number of retry attempts made.
Thank you for considering to help out with the source code! If you'd like to contribute, please fork, fix, commit and send a pull request for the maintainers to review and merge into the main code base.
- Setting up Git
- Fork the repository
- Open an issue if you encounter a bug or have a suggestion for improvements/features
STMSharp source code is available under MIT License, see license in the source.
Please contact at francesco.delre[at]protonmail.com for any details.