Alternative to SemaphoreSlim added

This commit is contained in:
mlemanczyk committed 2025-11-30 15:41:00 +01:00
1 parent c3254e3fda
commit 353a121d03
2 files changed
+159

No files matched your search

@@ -0,0 +1,56 @@
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PollingSemaphoreTests
{
[Fact]
[Trait("Category", "Fast")]
public void TryEnter_AllowsConfiguredConcurrency()
{
var semaphore = new PollingSemaphore(2, TimeSpan.Zero);
semaphore.TryEnter().Should().BeTrue();
semaphore.TryEnter().Should().BeTrue();
semaphore.TryEnter().Should().BeFalse();
semaphore.Release();
semaphore.Release();
}
[Fact]
[Trait("Category", "Fast")]
public async Task Wait_BlocksUntilRelease()
{
var semaphore = new PollingSemaphore(1, TimeSpan.FromMilliseconds(1));
semaphore.TryEnter().Should().BeTrue();
Task<bool> waiter = Task.Run(() => semaphore.Wait(TimeSpan.FromMilliseconds(200)));
await Task.Delay(20);
semaphore.Release();
bool entered = await waiter;
entered.Should().BeTrue();
semaphore.Release();
}
[Fact]
[Trait("Category", "Fast")]
public void Wait_ReturnsFalseWhenTimeoutHits()
{
var semaphore = new PollingSemaphore(1, TimeSpan.FromMilliseconds(2));
semaphore.TryEnter().Should().BeTrue();
bool acquired = semaphore.Wait(TimeSpan.FromMilliseconds(30));
acquired.Should().BeFalse();
semaphore.Release();
}
}
+103
View File
@@ -0,0 +1,103 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
/// <summary>
/// Lightweight semaphore-like guard that uses spinning and optional sleeps instead of locks.
/// It allows a small transient oversubscription while contending threads back off between checks.
/// </summary>
public sealed class PollingSemaphore
{
private readonly int _maximumConcurrency;
private readonly TimeSpan _pauseBetweenChecks;
private readonly bool _hasPause;
private int _currentHolders;
public PollingSemaphore(int maximumConcurrency, TimeSpan pauseBetweenChecks)
{
if (maximumConcurrency <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maximumConcurrency));
}
if (pauseBetweenChecks < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(pauseBetweenChecks));
}
_maximumConcurrency = maximumConcurrency;
_pauseBetweenChecks = pauseBetweenChecks;
_hasPause = pauseBetweenChecks > TimeSpan.Zero;
}
public bool TryEnter()
{
int newValue = Interlocked.Increment(ref _currentHolders);
if (newValue <= _maximumConcurrency)
{
return true;
}
Interlocked.Decrement(ref _currentHolders);
return false;
}
public bool Wait(TimeSpan timeout, CancellationToken cancellationToken = default)
{
ArgumentOutOfRangeException.ThrowIfLessThan(timeout, Timeout.InfiniteTimeSpan);
Stopwatch? stopwatch = timeout == Timeout.InfiniteTimeSpan ? null : Stopwatch.StartNew();
SpinWait spinner = new();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
if (TryEnter())
{
return true;
}
if (stopwatch is not null && stopwatch.Elapsed >= timeout)
{
return false;
}
if (spinner.Count < 10)
{
spinner.SpinOnce();
continue;
}
if (_hasPause)
{
Thread.Sleep(_pauseBetweenChecks);
spinner.Reset();
}
else
{
Thread.Yield();
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Wait(CancellationToken cancellationToken) => Wait(Timeout.InfiniteTimeSpan, cancellationToken);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Wait() => Wait(Timeout.InfiniteTimeSpan, CancellationToken.None);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Release()
{
int newValue = Interlocked.Decrement(ref _currentHolders);
if (newValue < 0)
{
Interlocked.Increment(ref _currentHolders);
throw new SynchronizationLockException("Release called more times than Wait.");
}
}
}