Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Tests;
public class Tests
{
[Fact]
public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsIncorrect()
public void GivenBalanceWithoutSync_WhenMultipleWithdrawals_ThenFinalBalanceIsNotGuaranteed()
{
var account = new Account
{
Expand All @@ -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
{
Expand All @@ -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]
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -10,13 +10,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="10.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ public static void WithdrawBalance(Action<int> 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);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace VolatileInterlockedLockInCsharp;

public sealed class OneShotInitializer
{
private int _started;

public bool TryStart() => Interlocked.Exchange(ref _started, 1) == 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading