From ec8d359494d596e31df3b1d489cc763d55b22af1 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 10 Oct 2026 21:52:54 +0200 Subject: [PATCH 1/2] VolatileInterlockedLockInCsharp: .NET 10, a harness that races, Lock type, Exchange and CompareExchange examples --- .../Tests/Tests.cs | 45 +++++++++++++++++-- .../Tests/Tests.csproj | 10 ++--- .../Account.cs | 19 +++++++- .../AccountService.cs | 7 +-- .../OneShotInitializer.cs | 8 ++++ .../Program.cs | 26 +++++++++++ .../VolatileInterlockedLockInCsharp.csproj | 2 +- 7 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/OneShotInitializer.cs diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.cs index 87d7905184..5c83962ca0 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.cs +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.cs @@ -5,7 +5,7 @@ namespace Tests; public class Tests { [Fact] - public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsIncorrect() + public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsNotGuaranteed() { var account = new Account { @@ -14,11 +14,12 @@ public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsIn AccountService.WithdrawBalance(account.Withdraw); - Assert.NotEqual(100000, account.Balance); + // A race is not deterministic: this demonstrates the race, it cannot prove it happened. + Assert.InRange(account.Balance, 0, 100000); } [Fact] - public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsIncorrect() + public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsNotGuaranteed() { var account = new Account { @@ -27,7 +28,8 @@ public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsIncor AccountService.WithdrawBalance(account.WithdrawVolatile); - Assert.NotEqual(1000, account.BalanceVolatile); + // A race is not deterministic: this demonstrates the race, it cannot prove it happened. + Assert.InRange(account.BalanceVolatile, 0, 100000); } [Fact] @@ -55,4 +57,39 @@ public void GivenBalanceWithInterlocked_WhenMultipleWithdrawals_ThenFinalBalance Assert.Equal(0, account.BalanceInterlocked); } + + [Fact] + public void GivenCompareExchangeLoop_WhenConcurrentWithdrawals_ThenEveryWithdrawalSucceedsAndBalanceIsZero() + { + var account = new Account + { + BalanceInterlocked = 100000 + }; + var succeeded = 0; + + Parallel.For(0, 1000, _ => + { + if (account.WithdrawIfSufficient(100)) + Interlocked.Increment(ref succeeded); + }); + + Assert.Equal(1000, succeeded); + Assert.Equal(0, account.BalanceInterlocked); + Assert.False(account.WithdrawIfSufficient(100)); + } + + [Fact] + public void GivenOneShotInitializer_WhenManyThreadsTryToStart_ThenExactlyOneWins() + { + var initializer = new OneShotInitializer(); + var winners = 0; + + Parallel.For(0, 1000, _ => + { + if (initializer.TryStart()) + Interlocked.Increment(ref winners); + }); + + Assert.Equal(1, winners); + } } \ No newline at end of file diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.csproj b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.csproj index d3f5ef5825..cb80726f4d 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.csproj +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Account.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Account.cs index 1724e719c2..673da39780 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Account.cs +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Account.cs @@ -6,7 +6,7 @@ public class Account private volatile int _balanceVolatile; private int _balanceLock; private int _balanceInterlocked; - private object _lock = new object(); + private readonly Lock _lock = new(); public int Balance { @@ -66,4 +66,21 @@ public void WithdrawInterlocked(int amount) { Interlocked.Add(ref _balanceInterlocked, -amount); } + + public bool WithdrawIfSufficient(int amount) + { + var current = Volatile.Read(ref _balanceInterlocked); + + while (current >= amount) + { + var actual = Interlocked.CompareExchange(ref _balanceInterlocked, current - amount, current); + + if (actual == current) + return true; + + current = actual; + } + + return false; + } } \ No newline at end of file diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/AccountService.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/AccountService.cs index 4251672f99..a9b8ae679b 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/AccountService.cs +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/AccountService.cs @@ -6,15 +6,16 @@ public static void WithdrawBalance(Action withdrawalAction) { using var synch = new ManualResetEventSlim(false); - var tasks = new Task[1000]; + var tasks = new Task[10]; for (var i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { synch.Wait(); - Thread.Sleep(Random.Shared.Next(50, 300)); - withdrawalAction(100); + + for (var j = 0; j < 100; j++) + withdrawalAction(100); }); } diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/OneShotInitializer.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/OneShotInitializer.cs new file mode 100644 index 0000000000..b4a0bc895f --- /dev/null +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/OneShotInitializer.cs @@ -0,0 +1,8 @@ +namespace VolatileInterlockedLockInCsharp; + +public sealed class OneShotInitializer +{ + private int _started; + + public bool TryStart() => Interlocked.Exchange(ref _started, 1) == 0; +} diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs index 23f58c6404..03669bab55 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs @@ -27,5 +27,31 @@ static void Main(string[] args) Console.WriteLine("Withdrawal with the 'Interlocked' class"); AccountService.WithdrawBalance(account.WithdrawInterlocked); Console.WriteLine($"Final balance: {account.BalanceInterlocked}"); + + var initializer = new OneShotInitializer(); + var winners = 0; + + Parallel.For(0, 1000, _ => + { + if (initializer.TryStart()) + Interlocked.Increment(ref winners); + }); + + Console.WriteLine($"Threads that won the initialization: {winners}"); + + var guardedAccount = new Account + { + BalanceInterlocked = 100000 + }; + var approved = 0; + + Parallel.For(0, 1100, _ => + { + if (guardedAccount.WithdrawIfSufficient(100)) + Interlocked.Increment(ref approved); + }); + + Console.WriteLine($"Approved withdrawals: {approved} of 1100"); + Console.WriteLine($"Final balance: {guardedAccount.BalanceInterlocked}"); } } \ No newline at end of file diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp.csproj b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp.csproj index f02677bf64..dfb40caafc 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp.csproj +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable From 2dd4df715953dce42b4081c9ef4ec25d7f508dda Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 10 Oct 2026 22:03:11 +0200 Subject: [PATCH 2/2] VolatileInterlockedLockInCsharp: label the one-shot count as calls --- .../VolatileInterlockedLockInCsharp/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs index 03669bab55..844be228ec 100644 --- a/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs +++ b/csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs @@ -37,7 +37,7 @@ static void Main(string[] args) Interlocked.Increment(ref winners); }); - Console.WriteLine($"Threads that won the initialization: {winners}"); + Console.WriteLine($"Calls that won the initialization: {winners}"); var guardedAccount = new Account {