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..844be228ec 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($"Calls 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