Skip to content

Commit ec8d359

Browse files
VolatileInterlockedLockInCsharp: .NET 10, a harness that races, Lock type, Exchange and CompareExchange examples
1 parent 2d403b5 commit ec8d359

7 files changed

Lines changed: 103 additions & 14 deletions

File tree

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.cs‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Tests;
55
public class Tests
66
{
77
[Fact]
8-
public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsIncorrect()
8+
public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsNotGuaranteed()
99
{
1010
var account = new Account
1111
{
@@ -14,11 +14,12 @@ public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsIn
1414

1515
AccountService.WithdrawBalance(account.Withdraw);
1616

17-
Assert.NotEqual(100000, account.Balance);
17+
// A race is not deterministic: this demonstrates the race, it cannot prove it happened.
18+
Assert.InRange(account.Balance, 0, 100000);
1819
}
1920

2021
[Fact]
21-
public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsIncorrect()
22+
public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsNotGuaranteed()
2223
{
2324
var account = new Account
2425
{
@@ -27,7 +28,8 @@ public void GivenBalanceVolatile_WhenMultipleWithdrawals_ThenFinalBalanceIsIncor
2728

2829
AccountService.WithdrawBalance(account.WithdrawVolatile);
2930

30-
Assert.NotEqual(1000, account.BalanceVolatile);
31+
// A race is not deterministic: this demonstrates the race, it cannot prove it happened.
32+
Assert.InRange(account.BalanceVolatile, 0, 100000);
3133
}
3234

3335
[Fact]
@@ -55,4 +57,39 @@ public void GivenBalanceWithInterlocked_WhenMultipleWithdrawals_ThenFinalBalance
5557

5658
Assert.Equal(0, account.BalanceInterlocked);
5759
}
60+
61+
[Fact]
62+
public void GivenCompareExchangeLoop_WhenConcurrentWithdrawals_ThenEveryWithdrawalSucceedsAndBalanceIsZero()
63+
{
64+
var account = new Account
65+
{
66+
BalanceInterlocked = 100000
67+
};
68+
var succeeded = 0;
69+
70+
Parallel.For(0, 1000, _ =>
71+
{
72+
if (account.WithdrawIfSufficient(100))
73+
Interlocked.Increment(ref succeeded);
74+
});
75+
76+
Assert.Equal(1000, succeeded);
77+
Assert.Equal(0, account.BalanceInterlocked);
78+
Assert.False(account.WithdrawIfSufficient(100));
79+
}
80+
81+
[Fact]
82+
public void GivenOneShotInitializer_WhenManyThreadsTryToStart_ThenExactlyOneWins()
83+
{
84+
var initializer = new OneShotInitializer();
85+
var winners = 0;
86+
87+
Parallel.For(0, 1000, _ =>
88+
{
89+
if (initializer.TryStart())
90+
Interlocked.Increment(ref winners);
91+
});
92+
93+
Assert.Equal(1, winners);
94+
}
5895
}

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/Tests/Tests.csproj‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

@@ -10,13 +10,13 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
14-
<PackageReference Include="xunit" Version="2.4.2" />
15-
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
1616
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1717
<PrivateAssets>all</PrivateAssets>
1818
</PackageReference>
19-
<PackageReference Include="coverlet.collector" Version="3.2.0">
19+
<PackageReference Include="coverlet.collector" Version="10.1.0">
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2121
<PrivateAssets>all</PrivateAssets>
2222
</PackageReference>

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Account.cs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class Account
66
private volatile int _balanceVolatile;
77
private int _balanceLock;
88
private int _balanceInterlocked;
9-
private object _lock = new object();
9+
private readonly Lock _lock = new();
1010

1111
public int Balance
1212
{
@@ -66,4 +66,21 @@ public void WithdrawInterlocked(int amount)
6666
{
6767
Interlocked.Add(ref _balanceInterlocked, -amount);
6868
}
69+
70+
public bool WithdrawIfSufficient(int amount)
71+
{
72+
var current = Volatile.Read(ref _balanceInterlocked);
73+
74+
while (current >= amount)
75+
{
76+
var actual = Interlocked.CompareExchange(ref _balanceInterlocked, current - amount, current);
77+
78+
if (actual == current)
79+
return true;
80+
81+
current = actual;
82+
}
83+
84+
return false;
85+
}
6986
}

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/AccountService.cs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ public static void WithdrawBalance(Action<int> withdrawalAction)
66
{
77
using var synch = new ManualResetEventSlim(false);
88

9-
var tasks = new Task[1000];
9+
var tasks = new Task[10];
1010

1111
for (var i = 0; i < tasks.Length; i++)
1212
{
1313
tasks[i] = Task.Run(() =>
1414
{
1515
synch.Wait();
16-
Thread.Sleep(Random.Shared.Next(50, 300));
17-
withdrawalAction(100);
16+
17+
for (var j = 0; j < 100; j++)
18+
withdrawalAction(100);
1819
});
1920
}
2021

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace VolatileInterlockedLockInCsharp;
2+
3+
public sealed class OneShotInitializer
4+
{
5+
private int _started;
6+
7+
public bool TryStart() => Interlocked.Exchange(ref _started, 1) == 0;
8+
}

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/Program.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,31 @@ static void Main(string[] args)
2727
Console.WriteLine("Withdrawal with the 'Interlocked' class");
2828
AccountService.WithdrawBalance(account.WithdrawInterlocked);
2929
Console.WriteLine($"Final balance: {account.BalanceInterlocked}");
30+
31+
var initializer = new OneShotInitializer();
32+
var winners = 0;
33+
34+
Parallel.For(0, 1000, _ =>
35+
{
36+
if (initializer.TryStart())
37+
Interlocked.Increment(ref winners);
38+
});
39+
40+
Console.WriteLine($"Threads that won the initialization: {winners}");
41+
42+
var guardedAccount = new Account
43+
{
44+
BalanceInterlocked = 100000
45+
};
46+
var approved = 0;
47+
48+
Parallel.For(0, 1100, _ =>
49+
{
50+
if (guardedAccount.WithdrawIfSufficient(100))
51+
Interlocked.Increment(ref approved);
52+
});
53+
54+
Console.WriteLine($"Approved withdrawals: {approved} of 1100");
55+
Console.WriteLine($"Final balance: {guardedAccount.BalanceInterlocked}");
3056
}
3157
}

‎csharp-advanced-topics/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp/VolatileInterlockedLockInCsharp.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net7.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
</PropertyGroup>

0 commit comments

Comments
 (0)