Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The runtime contract
Reference-type CompareExchange compares reference identity, even when the referenced class supplies value equality. An immutable candidate can be published only if the observed reference is still the expected one. On failure, a retry loop must recompute from the newly observed state and keep nonrepeatable effects outside the speculative computation. The portable sample checks the real identity rule in one flow. The desktop project runs bounded concurrent updates and counts one local effect after each successful publication. Neither version claims a database transaction, global idempotency, or exactly-once external delivery simply because a CAS succeeded.
Trace the portable program
The first exchange replaces the original zero state with the candidate containing one, so published is true. A new lookalike state also contains one and is value-equal to current, but it is a different reference. Using that lookalike as the expected CAS value therefore fails and leaves current at one. Stdout is published:True, value-equal:True, reference-match:False, and current:1. The external workers instead repeat successful immutable increments until two hundred updates and two hundred local effects are recorded. Candidate attempts may exceed that count and are reported separately as observations, not fixed scheduling predictions.
Execute the actual desktop lab
The desktop workers repeatedly read the current immutable reference, build a fresh candidate, and attempt publication. Only a successful CompareExchange authorizes the local notice increment. Failed candidates may be recomputed, and their attempt count is an actual variable diagnostic. In the recorded run, contention need not force an extra attempt, so the program does not require retries to be positive. The separate portable lookalike experiment already exercises a failed identity match deterministically. Together these examples establish bounded behavior and identity semantics without pretending a scheduler must produce one particular interleaving or one fixed allocation count.
Pitfalls and boundary limits
A retryable computation must not perform a nonrepeatable external effect before it knows publication succeeded. Even moving an effect after CAS does not create a durable transaction: failure between publication and delivery is still possible. Immutable state also matters; mutating a published object's fields in place can destroy the snapshot on which a reader reasons. ABA is another protocol concern when an old reference can return to the location. This particular increasing-count loop always publishes new states and never restores prior references. Do not generalize that design choice into a claim that CompareExchange universally detects every history of intermediate changes.
Practice one: isolate behavior
First replace the lookalike expected reference in the portable failure case with the actual current reference and predict successful publication of count two. Hint: keep value equality and reference identity as separate observations. Then deliberately use a new value-equal record again and verify failure without mutation. Preserve both the returned reference and final state when testing. A boolean derived from record equality would be the wrong success test here because it could accept an old value-equivalent object even when the exact expected reference was not present.
Practice two: design and verify
For another exercise, add a derived total to the immutable state and update both fields through one published candidate. Define the invariant readers should observe and keep all fields immutable after construction. Hint: do not write one field separately after successful CAS, which would reintroduce a partially published state. If the operation requests a real external effect, define a durable publication or idempotency boundary appropriate to that system instead of calling an in-memory notice counter an outbox. Test stale references and invalid candidates deliberately, and label measured retries as environment observations rather than expected scheduling facts.
Review the evidence
Review a CAS loop by identifying the exact expected reference, immutable candidate, success comparison, retry computation, and committed-effect boundary. Check whether old references can be republished and whether any mutable object reachable from state can change behind a reader. The portable output proves identity differs from record equality; the desktop project proves its bounded final counts. Neither establishes fairness, a universal retry rate, or exactly-once external effects. Correct lock-free reasoning needs a protocol and progress argument, while production reliability additionally needs failure handling beyond a successful atomic reference replacement.
CAS publication compares reference identity — desktop runtime lab
Set up the SDK project
- Save both files at their exact paths in a fresh owned directory.
- Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
- Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.
Download complete SDK project ↗
Project files
Program.cs
Download file ↗using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
State current = new(0);
int notices = 0, attempts = 0;
void Increment()
{
for (int item = 0; item < 50; item++)
{
while (true)
{
State before = Volatile.Read(ref current);
var next = new State(checked(before.Count + 1));
Interlocked.Increment(ref attempts);
if (!ReferenceEquals(Interlocked.CompareExchange(ref current, next, before), before)) continue;
Interlocked.Increment(ref notices); break;
}
}
}
OwnedThreads.Run(Increment, Increment, Increment, Increment);
Console.WriteLine($"current:{current.Count}");
Console.WriteLine($"committed-effects:{notices}");
Console.Error.WriteLine($"candidate-attempts:{attempts}");
sealed record State(int Count);
public static class OwnedThreads
{
public static void Run(params Action[] actions)
{
var errors = new ConcurrentQueue<Exception>();
var threads = actions.Select(action => new Thread(() => { try { action(); } catch (Exception error) { errors.Enqueue(error); } }) { IsBackground = true }).ToArray();
foreach (var thread in threads) thread.Start();
foreach (var thread in threads) if (!thread.Join(TimeSpan.FromSeconds(8))) throw new TimeoutException("Owned worker did not finish");
if (!errors.IsEmpty) throw new AggregateException(errors);
}
}
RuntimeLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>false</PublishAot>
</PropertyGroup>
</Project>
Expected result
Exact behavioral stdout on the stated desktop lab: current:200 committed-effects:200 Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
State current = new(0);
State original = current;
State candidate = new(1);
bool first = ReferenceEquals(Interlocked.CompareExchange(ref current, candidate, original), original);
State lookalike = new(1);
bool sameValue = current == lookalike;
bool matched = ReferenceEquals(Interlocked.CompareExchange(ref current, new State(2), lookalike), lookalike);
Console.WriteLine($"published:{first}");
Console.WriteLine($"value-equal:{sameValue}");
Console.WriteLine($"reference-match:{matched}");
Console.WriteLine($"current:{current.Count}");
sealed record State(int Count);
C# practice output
published:True value-equal:True reference-match:False current:1
Record equality does not change CompareExchange reference matching. In a retry loop, do not send an external effect every time a candidate is computed.
Further reading
Interview questions for this lesson
- Explain: Why can value-equal records fail reference CompareExchange, and what belongs outside a retry computation?
- Trace: Explain the successful first publication and the failed value-equal expected reference.
- Design: Design a retryable immutable-state update with a clear effect and progress boundary.