From 15bcde1776cac27375279c29ee02c3dc2383836a Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Fri, 12 Dec 2025 03:41:13 +0100 Subject: [PATCH] SemaphoreSlim replaced with PollingSemaphore. --- .../Gpu/Accelerators/AcceleratorStreamPool.cs | 4 +- PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs | 93 ++++++++++--------- PerfectNumbers.Core/PollingSemaphore.cs | 9 +- PerfectNumbers.Core/TaskThreadPool.cs | 2 +- 4 files changed, 57 insertions(+), 51 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Accelerators/AcceleratorStreamPool.cs b/PerfectNumbers.Core/Gpu/Accelerators/AcceleratorStreamPool.cs index 44af314..d4c9c57 100644 --- a/PerfectNumbers.Core/Gpu/Accelerators/AcceleratorStreamPool.cs +++ b/PerfectNumbers.Core/Gpu/Accelerators/AcceleratorStreamPool.cs @@ -6,10 +6,10 @@ namespace PerfectNumbers.Core.Gpu.Accelerators; public static class AcceleratorStreamPool { private static readonly Accelerator[] _accelerators = AcceleratorPool.Shared.Accelerators; - private static readonly SemaphoreSlim[] _locks = new SemaphoreSlim[PerfectNumberConstants.RollingAccelerators]; + private static readonly PollingSemaphore[] _locks = new PollingSemaphore[PerfectNumberConstants.RollingAccelerators]; private static readonly ConcurrentFixedCapacityStack[] _streams = new ConcurrentFixedCapacityStack[PerfectNumberConstants.RollingAccelerators]; - private static SemaphoreSlim CreateLock() => new(PerfectNumberConstants.ThreadsByAccelerator); + private static PollingSemaphore CreateLock() => new(PerfectNumberConstants.ThreadsByAccelerator, TimeSpan.FromMilliseconds(1000), 1); // private static int _rented; diff --git a/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs b/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs index 684a25e..68a9b90 100644 --- a/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs +++ b/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs @@ -1,58 +1,59 @@ +using System.Runtime.CompilerServices; + namespace PerfectNumbers.Core.Gpu; public static class GpuWorkLimiter { - private static SemaphoreSlim _semaphore = new(1, int.MaxValue); - private static readonly object _lock = new(); - private static int _currentLimit = 1; + private static readonly TimeSpan _pauseBetweenChecks = TimeSpan.FromSeconds(30); + private static PollingSemaphore _semaphore = new(int.MaxValue, _pauseBetweenChecks, 1); + private static readonly object _lock = new(); + private static int _currentLimit = 1; - public static Lease Acquire() - { - // TODO: Replace the per-call Lease allocation with the pooled struct-based guard from the - // GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the - // GPU scanning hot path. - var sem = _semaphore; - sem.Wait(); - return new Lease(sem); - } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static Lease Acquire() + { + // TODO: Replace the per-call Lease allocation with the pooled struct-based guard from the + // GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the + // GPU scanning hot path. + var sem = _semaphore; + sem.Wait(); + return new Lease(sem); + } - public static void SetLimit(int value) - { - if (value < 1) - { - value = 1; - } + public static void SetLimit(int value) + { + if (value < 1) + { + value = 1; + } - lock (_lock) - { - if (value == _currentLimit) - { - return; - } + lock (_lock) + { + if (value == _currentLimit) + { + return; + } - // Swap to a new semaphore for the new limit. We intentionally - // do not dispose the old semaphore to avoid releasing handles - // that may still be in use by existing Lease instances. - // TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the - // limiter state whenever limits are adjusted from the CLI. - _semaphore = new SemaphoreSlim(value, value); - _currentLimit = value; - } - } + // Swap to a new semaphore for the new limit. We intentionally + // do not dispose the old semaphore to avoid releasing handles + // that may still be in use by existing Lease instances. + // TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the + // limiter state whenever limits are adjusted from the CLI. + _semaphore = new PollingSemaphore(value, _pauseBetweenChecks, 1); + _currentLimit = value; + } + } - public sealed class Lease - { - private readonly SemaphoreSlim _sem; + [method: MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly struct Lease(PollingSemaphore sem) + { + private readonly PollingSemaphore _sem = sem; - internal Lease(SemaphoreSlim sem) - { - _sem = sem; - } - - public void Dispose() - { - _sem.Release(); - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void Dispose() + { + _sem.Release(); + } + } } diff --git a/PerfectNumbers.Core/PollingSemaphore.cs b/PerfectNumbers.Core/PollingSemaphore.cs index 3259b9e..1d7edd2 100644 --- a/PerfectNumbers.Core/PollingSemaphore.cs +++ b/PerfectNumbers.Core/PollingSemaphore.cs @@ -11,17 +11,21 @@ public sealed class PollingSemaphore { private readonly int _maximumConcurrency; private readonly TimeSpan _pauseBetweenChecks; + private readonly int _attemptsBeforeSleep; + private readonly bool _hasPause; private int _currentHolders; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public PollingSemaphore(int maximumConcurrency, TimeSpan pauseBetweenChecks) + public PollingSemaphore(int maximumConcurrency, TimeSpan pauseBetweenChecks, int attemptsBeforeSleep = 10) { ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(maximumConcurrency, 0); ArgumentOutOfRangeException.ThrowIfLessThan(pauseBetweenChecks, TimeSpan.Zero); _maximumConcurrency = maximumConcurrency; _pauseBetweenChecks = pauseBetweenChecks; + _attemptsBeforeSleep = attemptsBeforeSleep; + _hasPause = pauseBetweenChecks > TimeSpan.Zero; } @@ -45,6 +49,7 @@ public sealed class PollingSemaphore bool hasPause = _hasPause; bool hasStopwatch = timeout != Timeout.InfiniteTimeSpan; + int attemptsBeforeSleep = _attemptsBeforeSleep; SpinWait spinner = new(); TimeSpan pauseBetweenChecks = _pauseBetweenChecks; Stopwatch? stopwatch = hasStopwatch ? Stopwatch.StartNew() : null; @@ -62,7 +67,7 @@ public sealed class PollingSemaphore return false; } - if (spinner.Count < 10) + if (spinner.Count < attemptsBeforeSleep) { spinner.SpinOnce(); continue; diff --git a/PerfectNumbers.Core/TaskThreadPool.cs b/PerfectNumbers.Core/TaskThreadPool.cs index ff67920..f746ee4 100644 --- a/PerfectNumbers.Core/TaskThreadPool.cs +++ b/PerfectNumbers.Core/TaskThreadPool.cs @@ -10,7 +10,7 @@ internal sealed class TaskThreadPool public readonly Task Task = task; } - private readonly ConcurrentFixedCapacityStack _pendingTaskQueue = new(PerfectNumberConstants.DefaultThreadPoolCapacity); + private readonly ConcurrentFixedCapacityStack _pendingTaskQueue = new(PerfectNumberConstants.DefaultThreadPoolCapacity << 1); private readonly HashSet _runningThreads = []; private readonly object _lock = new(); private readonly Action _taskExecutor;