From 2178a5c88f83fb384d6c2acbd5b377c2316f114f Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 7 Jul 2025 17:09:02 -0700 Subject: [PATCH 01/13] Extend allowed SemaphoreSlim wait TimeSpam values to match Timer and other APIs --- .../src/System/Threading/SemaphoreSlim.cs | 81 ++++++++++++------- .../src/System/Threading/TimeoutHelper.cs | 23 +++++- 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index bab6bbd332b19a..f7c45c3957c910 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -178,7 +178,7 @@ public void Wait() if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.Infinite, CancellationToken.None); + Wait(Timeout.UnsignedInfinite, CancellationToken.None); } /// @@ -198,7 +198,7 @@ public void Wait(CancellationToken cancellationToken) if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.Infinite, cancellationToken); + Wait(Timeout.UnsignedInfinite, cancellationToken); } /// @@ -212,7 +212,7 @@ public void Wait(CancellationToken cancellationToken) /// otherwise, false. /// is a negative /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater - /// than . + /// than maximum allowed timer duration. [UnsupportedOSPlatform("browser")] public bool Wait(TimeSpan timeout) { @@ -221,14 +221,14 @@ public bool Wait(TimeSpan timeout) #endif // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue) + if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } // Call wait with the timeout milliseconds - return Wait((int)timeout.TotalMilliseconds, CancellationToken.None); + return Wait(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, CancellationToken.None); } /// @@ -245,7 +245,7 @@ public bool Wait(TimeSpan timeout) /// otherwise, false. /// is a negative /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater - /// than . + /// than maximum allowed timer duration. /// was canceled. [UnsupportedOSPlatform("browser")] public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) @@ -255,14 +255,14 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) #endif // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue) + if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } // Call wait with the timeout milliseconds - return Wait((int)timeout.TotalMilliseconds, cancellationToken); + return Wait(totalMilliseconds == timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); } /// @@ -302,10 +302,6 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) #if TARGET_WASI if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif - CheckDispose(); -#if FEATURE_WASM_MANAGED_THREADS - Thread.AssureBlockingPossible(); -#endif if (millisecondsTimeout < -1) { @@ -313,6 +309,15 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } + return Wait(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); + } + + private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) + { + CheckDispose(); +#if FEATURE_WASM_MANAGED_THREADS + Thread.AssureBlockingPossible(); +#endif cancellationToken.ThrowIfCancellationRequested(); // Perf: Check the stack timeout parameter before checking the volatile count @@ -323,7 +328,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) } uint startTime = 0; - if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0) + if (millisecondsTimeout != Timeout.UnsignedInfinite && millisecondsTimeout > 0) { startTime = TimeoutHelper.GetTime(); } @@ -449,12 +454,12 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) /// The CancellationToken to observe. /// true if the monitor received a signal, false if the timeout expired [UnsupportedOSPlatform("browser")] - private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, CancellationToken cancellationToken) + private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, uint startTime, CancellationToken cancellationToken) { #if TARGET_WASI if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif - int remainingWaitMilliseconds = Timeout.Infinite; + int monitorWaitMilliseconds = Timeout.Infinite; // Wait on the monitor as long as the count is zero while (m_currentCount == 0) @@ -462,17 +467,32 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca // If cancelled, we throw. Trying to wait could lead to deadlock. cancellationToken.ThrowIfCancellationRequested(); - if (millisecondsTimeout != Timeout.Infinite) + // Since Monitor.Wait will handle the actual wait and it accepts an int timeout, + // we may need to cap the timeout to int.MaxValue. + bool timeoutIsCapped = false; + if (millisecondsTimeout != Timeout.UnsignedInfinite) { - remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); + uint remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); if (remainingWaitMilliseconds <= 0) { // The thread has expires its timeout return false; } + if (remainingWaitMilliseconds <= int.MaxValue) + { + monitorWaitMilliseconds = (int)remainingWaitMilliseconds; + } + else + { + timeoutIsCapped = true; + monitorWaitMilliseconds = int.MaxValue; + } } - // ** the actual wait ** - bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, remainingWaitMilliseconds); + + + // The actual wait. If the timeout was capped and waitSuccessful is false, it doesn't imply + // a timeout, we are just limited by Monitor.Wait's maximum timeout value. + bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, monitorWaitMilliseconds); // This waiter has woken up and this needs to be reflected in the count of waiters pulsed to wake. Since we // don't have thread-specific pulse state, there is not enough information to tell whether this thread woke up @@ -485,7 +505,7 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca --m_countOfWaitersPulsedToWake; } - if (!waitSuccessful) + if (!timeoutIsCapped && !waitSuccessful) { return false; } @@ -500,7 +520,7 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca /// A task that will complete when the semaphore has been entered. public Task WaitAsync() { - return WaitAsync(Timeout.Infinite, default); + return WaitAsync(Timeout.UnsignedInfinite, default); } /// @@ -516,7 +536,7 @@ public Task WaitAsync() /// public Task WaitAsync(CancellationToken cancellationToken) { - return WaitAsync(Timeout.Infinite, cancellationToken); + return WaitAsync(Timeout.UnsignedInfinite, cancellationToken); } /// @@ -588,14 +608,14 @@ public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToke { // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue) + if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } // Call wait with the timeout milliseconds - return WaitAsync((int)timeout.TotalMilliseconds, cancellationToken); + return WaitAsync(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); } /// @@ -618,14 +638,19 @@ public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToke /// public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken) { - CheckDispose(); - if (millisecondsTimeout < -1) { throw new ArgumentOutOfRangeException( nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } + return WaitAsync(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); + } + + private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken) + { + CheckDispose(); + // Bail early for cancellation if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); @@ -651,7 +676,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat { Debug.Assert(m_currentCount == 0, "m_currentCount should never be negative"); TaskNode asyncWaiter = CreateAndAddAsyncWaiter(); - return (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled) ? + return (millisecondsTimeout == Timeout.UnsignedInfinite && !cancellationToken.CanBeCanceled) ? asyncWaiter : WaitUntilCountOrTimeoutAsync(asyncWaiter, millisecondsTimeout, cancellationToken); } @@ -716,7 +741,7 @@ private bool RemoveAsyncWaiter(TaskNode task) /// The timeout. /// The cancellation token. /// The task to return to the caller. - private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken) + private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, uint millisecondsTimeout, CancellationToken cancellationToken) { Debug.Assert(asyncWaiter is not null, "Waiter should have been constructed"); Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held"); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs index b17d38d44d7131..88ff917e8f4d16 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs @@ -26,7 +26,7 @@ public static uint GetTime() /// /// The first time (in milliseconds) observed when the wait started /// The original wait timeout in milliseconds - /// The new wait time in milliseconds, or -1 if the time expired + /// The new wait time in milliseconds public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTimeout) { // The function must be called in case the time out is not infinite @@ -49,5 +49,26 @@ public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTime return currentWaitTimeout; } + + /// + /// Helper function to measure and update the elapsed time + /// + /// The first time (in milliseconds) observed when the wait started + /// The original wait timeout in milliseconds + /// The new wait time in milliseconds + public static uint UpdateTimeOut(uint startTime, uint originalWaitMillisecondsTimeout) + { + // The function must be called in case the time out is not infinite + Debug.Assert(originalWaitMillisecondsTimeout != Timeout.UnsignedInfinite); + + uint elapsedMilliseconds = GetTime() - startTime; + + if (originalWaitMillisecondsTimeout <= elapsedMilliseconds) + { + return 0; + } + + return originalWaitMillisecondsTimeout - elapsedMilliseconds; + } } } From ef9dee671965f7e01e6670afa8e506306c451e6a Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 7 Jul 2025 17:40:12 -0700 Subject: [PATCH 02/13] Update SemaphoreSlim tests --- .../System.Private.CoreLib/src/Resources/Strings.resx | 3 +++ .../src/System/Threading/SemaphoreSlim.cs | 6 +++--- src/libraries/System.Threading/tests/SemaphoreSlimTests.cs | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 497a524ed303fa..1a3962be5e740c 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3410,6 +3410,9 @@ The timeout must represent a value between -1 and Int32.MaxValue, inclusive. + + The timeout must represent a value between -1 and Timer.MaxSupportedTimeout, inclusive. + Non existent ParameterInfo. Position bigger than member's parameters length. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index f7c45c3957c910..ff15c4d29bcabd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -224,7 +224,7 @@ public bool Wait(TimeSpan timeout) if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds @@ -258,7 +258,7 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds @@ -611,7 +611,7 @@ public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToke if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index faac4d9cf9f85b..3b1e8977d98918 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -60,6 +60,7 @@ public static void RunSemaphoreSlimTest1_Wait() RunSemaphoreSlimTest1_Wait_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(0, 10, 10, false, null); + RunSemaphoreSlimTest1_Wait_Helper(1, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout), true, null); } [Fact] @@ -68,7 +69,7 @@ public static void RunSemaphoreSlimTest1_Wait_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_Wait_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_Wait_Helper - (10, 10, new TimeSpan(0, 0, int.MaxValue), true, typeof(ArgumentOutOfRangeException)); + (10, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -87,6 +88,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(0, 10, 10, false, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout), true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -96,7 +98,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync_Helper - (10, 10, new TimeSpan(0, 0, int.MaxValue), true, typeof(ArgumentOutOfRangeException)); + (10, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync2(); } From 62d0789f8ffe1e8e0553d7affb4b6f23a57724d4 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 7 Jul 2025 17:50:46 -0700 Subject: [PATCH 03/13] Fix TimeSpan in tests --- .../src/System/Threading/SemaphoreSlim.cs | 2 +- .../System.Threading/tests/SemaphoreSlimTests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index ff15c4d29bcabd..2434f903ba06a1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -262,7 +262,7 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) } // Call wait with the timeout milliseconds - return Wait(totalMilliseconds == timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); + return Wait(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); } /// diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index 3b1e8977d98918..74d93c0d5cbf1e 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -60,7 +60,7 @@ public static void RunSemaphoreSlimTest1_Wait() RunSemaphoreSlimTest1_Wait_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_Wait_Helper(1, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout), true, null); } [Fact] @@ -69,7 +69,7 @@ public static void RunSemaphoreSlimTest1_Wait_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_Wait_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_Wait_Helper - (10, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); + (10, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -88,7 +88,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout), true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -98,7 +98,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync_Helper - (10, 10, new TimeSpan(0, 0, Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); + (10, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync2(); } From 8be5d50c73a56fcf056b834310368b99cd99b024 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 8 Jul 2025 00:32:19 -0700 Subject: [PATCH 04/13] Mark Wait as not supported on browser --- .../src/System/Threading/SemaphoreSlim.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index 2434f903ba06a1..a7d1c538a66124 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -312,6 +312,17 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) return Wait(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); } + /// + /// Blocks the current thread until it can enter the , + /// using a 32-bit unsigned integer to measure the time interval, + /// while observing a . + /// + /// The number of milliseconds to wait, or to + /// wait indefinitely. + /// The to observe. + /// true if the current thread successfully entered the ; otherwise, false. + /// was canceled. + [UnsupportedOSPlatform("browser")] private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose(); From 410a82c8a1aa7bc3885104c5a1d55aed9c1b3ebd Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 8 Jul 2025 01:21:29 -0700 Subject: [PATCH 05/13] Fix SemaphoreSlim tests --- .../System.Threading/tests/SemaphoreSlimTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index 74d93c0d5cbf1e..a8cd3b2fe4c35e 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -14,6 +14,8 @@ namespace System.Threading.Tests /// public class SemaphoreSlimTests { + private const uint TimerMaxSupportedTimeout = 0xfffffffe; + /// /// SemaphoreSlim public methods and properties to be tested /// @@ -60,7 +62,7 @@ public static void RunSemaphoreSlimTest1_Wait() RunSemaphoreSlimTest1_Wait_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout), true, null); } [Fact] @@ -69,7 +71,7 @@ public static void RunSemaphoreSlimTest1_Wait_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_Wait_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_Wait_Helper - (10, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); + (10, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -88,7 +90,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout), true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -98,7 +100,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync_NegativeCases() // Invalid timeout RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync_Helper - (10, 10, TimeSpan.FromMilliseconds(Timer.MaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); + (10, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync2(); } From 371aa063b1abf6f36555324c793f995e4e87def2 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 8 Jul 2025 08:57:32 -0700 Subject: [PATCH 06/13] Fix WaitUntilCountOrTimeoutAsync --- .../src/System/Threading/SemaphoreSlim.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index a7d1c538a66124..6b0c7d9b68fbb2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -757,7 +757,9 @@ private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, uint Debug.Assert(asyncWaiter is not null, "Waiter should have been constructed"); Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held"); - await ((Task)asyncWaiter.WaitAsync(TimeSpan.FromMilliseconds(millisecondsTimeout), cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + await ((Task)asyncWaiter.WaitAsync( + TimeSpan.FromMilliseconds(millisecondsTimeout == Timeout.UnsignedInfinite ? (long)Timeout.Infinite : (long)millisecondsTimeout), + cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); if (cancellationToken.IsCancellationRequested) { From b1fce6cc2156cddc6d7056083b563052fb3c6307 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 8 Jul 2025 09:38:36 -0700 Subject: [PATCH 07/13] Add summary to WaitAsync --- .../src/System/Threading/SemaphoreSlim.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index 6b0c7d9b68fbb2..ef0db03972cf19 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -658,6 +658,21 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat return WaitAsync(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); } + /// + /// Asynchronously waits to enter the , + /// using a 32-bit unsigned integer to measure the time interval, + /// while observing a . + /// + /// + /// The number of milliseconds to wait, or to wait indefinitely. + /// + /// The to observe. + /// + /// A task that will complete with a result of true if the current thread successfully entered + /// the , otherwise with a result of false. + /// + /// The current instance has already been + /// disposed. private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose(); From 70e66ac8f0b35338408e1340fdb993009cc713a9 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 15 Jul 2025 17:26:49 -0700 Subject: [PATCH 08/13] Address PR feedback --- .../src/Resources/Strings.resx | 2 +- .../src/System/Threading/SemaphoreSlim.cs | 8 ++++---- .../src/System/Threading/TimeoutHelper.cs | 12 ++++++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 1a3962be5e740c..a0ededcd616105 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3411,7 +3411,7 @@ The timeout must represent a value between -1 and Int32.MaxValue, inclusive. - The timeout must represent a value between -1 and Timer.MaxSupportedTimeout, inclusive. + The timeout must represent a value between -1 and the maximum allowed timer duration. Non existent ParameterInfo. Position bigger than member's parameters length. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index ef0db03972cf19..9f824e79cb8224 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -338,10 +338,10 @@ private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) return false; } - uint startTime = 0; + long startTime = 0; if (millisecondsTimeout != Timeout.UnsignedInfinite && millisecondsTimeout > 0) { - startTime = TimeoutHelper.GetTime(); + startTime = TimeoutHelper.GetTime64(); } bool waitSuccessful = false; @@ -465,7 +465,7 @@ private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) /// The CancellationToken to observe. /// true if the monitor received a signal, false if the timeout expired [UnsupportedOSPlatform("browser")] - private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, uint startTime, CancellationToken cancellationToken) + private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, long startTime, CancellationToken cancellationToken) { #if TARGET_WASI if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 @@ -483,7 +483,7 @@ private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, uint startTime, C bool timeoutIsCapped = false; if (millisecondsTimeout != Timeout.UnsignedInfinite) { - uint remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); + long remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); if (remainingWaitMilliseconds <= 0) { // The thread has expires its timeout diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs index 88ff917e8f4d16..d73cb3e9d05317 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs @@ -21,6 +21,14 @@ public static uint GetTime() return (uint)Environment.TickCount; } + /// + /// Returns as a start time in milliseconds. + /// + public static long GetTime64() + { + return Environment.TickCount64; + } + /// /// Helper function to measure and update the elapsed time /// @@ -56,12 +64,12 @@ public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTime /// The first time (in milliseconds) observed when the wait started /// The original wait timeout in milliseconds /// The new wait time in milliseconds - public static uint UpdateTimeOut(uint startTime, uint originalWaitMillisecondsTimeout) + public static long UpdateTimeOut(long startTime, uint originalWaitMillisecondsTimeout) { // The function must be called in case the time out is not infinite Debug.Assert(originalWaitMillisecondsTimeout != Timeout.UnsignedInfinite); - uint elapsedMilliseconds = GetTime() - startTime; + long elapsedMilliseconds = GetTime64() - startTime; if (originalWaitMillisecondsTimeout <= elapsedMilliseconds) { From 72107c8b0e65939219cfaea3883c6b71a57966e4 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 30 Jul 2025 20:56:06 -0700 Subject: [PATCH 09/13] Address PR feedback --- .../src/Resources/Strings.resx | 2 +- .../System/Threading/ManualResetEventSlim.cs | 5 +- .../src/System/Threading/SemaphoreSlim.cs | 48 ++++++++--------- .../src/System/Threading/SpinLock.cs | 2 +- .../src/System/Threading/SpinWait.cs | 4 +- .../src/System/Threading/TimeoutHelper.cs | 52 ++----------------- .../tests/SemaphoreSlimTests.cs | 10 +--- 7 files changed, 37 insertions(+), 86 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index a0ededcd616105..9a55152da25821 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3411,7 +3411,7 @@ The timeout must represent a value between -1 and Int32.MaxValue, inclusive. - The timeout must represent a value between -1 and the maximum allowed timer duration. + The timeout must be greater than or equal to -1. Non existent ParameterInfo. Position bigger than member's parameters length. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs index 78f83a11bf66b0..fcf7cc093f21b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @@ -520,7 +520,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) // period of time. The timeout adjustments only take effect when and if we actually // decide to block in the kernel below. - startTime = TimeoutHelper.GetTime(); + startTime = (uint)Environment.TickCount; bNeedTimeoutAdjustment = true; } @@ -558,7 +558,8 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) // update timeout (delays in wait commencement are due to spinning and/or spurious wakeups from other waits being canceled) if (bNeedTimeoutAdjustment) { - realMillisecondsTimeout = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); + // TimeoutHelper.UpdateTimeOut returns a long but the value is capped as millisecondsTimeout is an int. + realMillisecondsTimeout = (int)TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); if (realMillisecondsTimeout <= 0) return false; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index 9f824e79cb8224..a9c232f877cf92 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -178,7 +178,7 @@ public void Wait() if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.UnsignedInfinite, CancellationToken.None); + Wait(Timeout.Infinite, CancellationToken.None); } /// @@ -198,7 +198,7 @@ public void Wait(CancellationToken cancellationToken) if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.UnsignedInfinite, cancellationToken); + Wait(Timeout.Infinite, cancellationToken); } /// @@ -211,8 +211,7 @@ public void Wait(CancellationToken cancellationToken) /// true if the current thread successfully entered the ; /// otherwise, false. /// is a negative - /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater - /// than maximum allowed timer duration. + /// number other than -1 milliseconds, which represents an infinite time-out. [UnsupportedOSPlatform("browser")] public bool Wait(TimeSpan timeout) { @@ -221,14 +220,14 @@ public bool Wait(TimeSpan timeout) #endif // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) + if (totalMilliseconds < -1) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return Wait(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, CancellationToken.None); + return Wait(totalMilliseconds, CancellationToken.None); } /// @@ -244,8 +243,7 @@ public bool Wait(TimeSpan timeout) /// true if the current thread successfully entered the ; /// otherwise, false. /// is a negative - /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater - /// than maximum allowed timer duration. + /// number other than -1 milliseconds, which represents an infinite time-out. /// was canceled. [UnsupportedOSPlatform("browser")] public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) @@ -255,14 +253,14 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) #endif // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) + if (totalMilliseconds < -1) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return Wait(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); + return Wait(totalMilliseconds, cancellationToken); } /// @@ -309,7 +307,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } - return Wait(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); + return Wait(millisecondsTimeout, cancellationToken); } /// @@ -323,7 +321,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) /// true if the current thread successfully entered the ; otherwise, false. /// was canceled. [UnsupportedOSPlatform("browser")] - private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) + private bool Wait(long millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose(); #if FEATURE_WASM_MANAGED_THREADS @@ -339,9 +337,9 @@ private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) } long startTime = 0; - if (millisecondsTimeout != Timeout.UnsignedInfinite && millisecondsTimeout > 0) + if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0) { - startTime = TimeoutHelper.GetTime64(); + startTime = Environment.TickCount64; } bool waitSuccessful = false; @@ -465,7 +463,7 @@ private bool Wait(uint millisecondsTimeout, CancellationToken cancellationToken) /// The CancellationToken to observe. /// true if the monitor received a signal, false if the timeout expired [UnsupportedOSPlatform("browser")] - private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, long startTime, CancellationToken cancellationToken) + private bool WaitUntilCountOrTimeout(long millisecondsTimeout, long startTime, CancellationToken cancellationToken) { #if TARGET_WASI if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 @@ -481,7 +479,7 @@ private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, long startTime, C // Since Monitor.Wait will handle the actual wait and it accepts an int timeout, // we may need to cap the timeout to int.MaxValue. bool timeoutIsCapped = false; - if (millisecondsTimeout != Timeout.UnsignedInfinite) + if (millisecondsTimeout != Timeout.Infinite) { long remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); if (remainingWaitMilliseconds <= 0) @@ -531,7 +529,7 @@ private bool WaitUntilCountOrTimeout(uint millisecondsTimeout, long startTime, C /// A task that will complete when the semaphore has been entered. public Task WaitAsync() { - return WaitAsync(Timeout.UnsignedInfinite, default); + return WaitAsync(Timeout.Infinite, default); } /// @@ -547,7 +545,7 @@ public Task WaitAsync() /// public Task WaitAsync(CancellationToken cancellationToken) { - return WaitAsync(Timeout.UnsignedInfinite, cancellationToken); + return WaitAsync(Timeout.Infinite, cancellationToken); } /// @@ -619,14 +617,14 @@ public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToke { // Validate the timeout long totalMilliseconds = (long)timeout.TotalMilliseconds; - if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout) + if (totalMilliseconds < -1) { throw new ArgumentOutOfRangeException( nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return WaitAsync(totalMilliseconds == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)totalMilliseconds, cancellationToken); + return WaitAsync(totalMilliseconds, cancellationToken); } /// @@ -655,7 +653,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } - return WaitAsync(millisecondsTimeout == Timeout.Infinite ? Timeout.UnsignedInfinite : (uint)millisecondsTimeout, cancellationToken); + return WaitAsync(millisecondsTimeout, cancellationToken); } /// @@ -673,7 +671,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat /// /// The current instance has already been /// disposed. - private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken) + private Task WaitAsync(long millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose(); @@ -702,7 +700,7 @@ private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancell { Debug.Assert(m_currentCount == 0, "m_currentCount should never be negative"); TaskNode asyncWaiter = CreateAndAddAsyncWaiter(); - return (millisecondsTimeout == Timeout.UnsignedInfinite && !cancellationToken.CanBeCanceled) ? + return (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled) ? asyncWaiter : WaitUntilCountOrTimeoutAsync(asyncWaiter, millisecondsTimeout, cancellationToken); } @@ -767,13 +765,13 @@ private bool RemoveAsyncWaiter(TaskNode task) /// The timeout. /// The cancellation token. /// The task to return to the caller. - private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, uint millisecondsTimeout, CancellationToken cancellationToken) + private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long millisecondsTimeout, CancellationToken cancellationToken) { Debug.Assert(asyncWaiter is not null, "Waiter should have been constructed"); Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held"); await ((Task)asyncWaiter.WaitAsync( - TimeSpan.FromMilliseconds(millisecondsTimeout == Timeout.UnsignedInfinite ? (long)Timeout.Infinite : (long)millisecondsTimeout), + TimeSpan.FromMilliseconds(millisecondsTimeout), cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); if (cancellationToken.IsCancellationRequested) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs index c0eba40cb428af..a199479785b3da 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs @@ -290,7 +290,7 @@ private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken) uint startTime = 0; if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0) { - startTime = TimeoutHelper.GetTime(); + startTime = (uint)Environment.TickCount; } if (IsThreadOwnerTrackingEnabled) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinWait.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinWait.cs index 1922f67f93d04c..4246a33191d10b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinWait.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinWait.cs @@ -315,7 +315,7 @@ public static bool SpinUntil(Func condition, int millisecondsTimeout) uint startTime = 0; if (millisecondsTimeout != 0 && millisecondsTimeout != Timeout.Infinite) { - startTime = TimeoutHelper.GetTime(); + startTime = (uint)Environment.TickCount; } SpinWait spinner = default; while (!condition()) @@ -329,7 +329,7 @@ public static bool SpinUntil(Func condition, int millisecondsTimeout) if (millisecondsTimeout != Timeout.Infinite && spinner.NextSpinWillYield) { - if (millisecondsTimeout <= (TimeoutHelper.GetTime() - startTime)) + if (millisecondsTimeout <= (uint)Environment.TickCount - startTime) { return false; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs index d73cb3e9d05317..37500c4dfa5e82 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimeoutHelper.cs @@ -6,50 +6,29 @@ namespace System.Threading { /// - /// A helper class to capture a start time using as a time in milliseconds. - /// Also updates a given timeout by subtracting the current time from the start time. + /// A helper class to update a given timeout by subtracting the current time from the start time. /// internal static class TimeoutHelper { - /// - /// Returns as a start time in milliseconds as a . - /// rolls over from positive to negative every ~25 days, then ~25 days to back to positive again. - /// is used to ignore the sign and double the range to 50 days. - /// - public static uint GetTime() - { - return (uint)Environment.TickCount; - } - - /// - /// Returns as a start time in milliseconds. - /// - public static long GetTime64() - { - return Environment.TickCount64; - } - /// /// Helper function to measure and update the elapsed time /// /// The first time (in milliseconds) observed when the wait started /// The original wait timeout in milliseconds /// The new wait time in milliseconds - public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTimeout) + public static long UpdateTimeOut(long startTime, long originalWaitMillisecondsTimeout) { // The function must be called in case the time out is not infinite Debug.Assert(originalWaitMillisecondsTimeout != Timeout.Infinite); - uint elapsedMilliseconds = (GetTime() - startTime); + ulong elapsedMilliseconds = (ulong)(Environment.TickCount64 - startTime); - // Check the elapsed milliseconds is greater than max int because this property is uint - if (elapsedMilliseconds > int.MaxValue) + if (elapsedMilliseconds > long.MaxValue) { return 0; } - // Subtract the elapsed time from the current wait time - int currentWaitTimeout = originalWaitMillisecondsTimeout - (int)elapsedMilliseconds; + long currentWaitTimeout = originalWaitMillisecondsTimeout - (long)elapsedMilliseconds; if (currentWaitTimeout <= 0) { return 0; @@ -57,26 +36,5 @@ public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTime return currentWaitTimeout; } - - /// - /// Helper function to measure and update the elapsed time - /// - /// The first time (in milliseconds) observed when the wait started - /// The original wait timeout in milliseconds - /// The new wait time in milliseconds - public static long UpdateTimeOut(long startTime, uint originalWaitMillisecondsTimeout) - { - // The function must be called in case the time out is not infinite - Debug.Assert(originalWaitMillisecondsTimeout != Timeout.UnsignedInfinite); - - long elapsedMilliseconds = GetTime64() - startTime; - - if (originalWaitMillisecondsTimeout <= elapsedMilliseconds) - { - return 0; - } - - return originalWaitMillisecondsTimeout - elapsedMilliseconds; - } } } diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index a8cd3b2fe4c35e..c7b1a5447feead 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -14,8 +14,6 @@ namespace System.Threading.Tests /// public class SemaphoreSlimTests { - private const uint TimerMaxSupportedTimeout = 0xfffffffe; - /// /// SemaphoreSlim public methods and properties to be tested /// @@ -62,7 +60,7 @@ public static void RunSemaphoreSlimTest1_Wait() RunSemaphoreSlimTest1_Wait_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_Wait_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(uint.MaxValue), true, null); } [Fact] @@ -70,8 +68,6 @@ public static void RunSemaphoreSlimTest1_Wait_NegativeCases() { // Invalid timeout RunSemaphoreSlimTest1_Wait_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); - RunSemaphoreSlimTest1_Wait_Helper - (10, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -90,7 +86,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(0, 10, 10, false, null); - RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout), true, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(uint.MaxValue), true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -99,8 +95,6 @@ public static void RunSemaphoreSlimTest1_WaitAsync_NegativeCases() { // Invalid timeout RunSemaphoreSlimTest1_WaitAsync_Helper(10, 10, -10, true, typeof(ArgumentOutOfRangeException)); - RunSemaphoreSlimTest1_WaitAsync_Helper - (10, 10, TimeSpan.FromMilliseconds(TimerMaxSupportedTimeout + 1), true, typeof(ArgumentOutOfRangeException)); RunSemaphoreSlimTest1_WaitAsync2(); } From 321730cff85a0e999219de84a1e2ed20630539fd Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 31 Jul 2025 12:40:34 -0700 Subject: [PATCH 10/13] Fix error message --- src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | 2 +- .../src/System/Threading/SemaphoreSlim.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 9a55152da25821..e5d98648641517 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3411,7 +3411,7 @@ The timeout must represent a value between -1 and Int32.MaxValue, inclusive. - The timeout must be greater than or equal to -1. + The value needs to translate in milliseconds to -1 (signifying an infinite timeout), or be non-negative. Non existent ParameterInfo. Position bigger than member's parameters length. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index a9c232f877cf92..993a1b95882ad6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -653,7 +653,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } - return WaitAsync(millisecondsTimeout, cancellationToken); + return WaitAsync((long)millisecondsTimeout, cancellationToken); } /// From 9ffa13074100390c2a13a00eb2c38648ba5ce872 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 31 Jul 2025 13:45:34 -0700 Subject: [PATCH 11/13] Use Environment.TickCount64 for start times --- .../src/System/Threading/ManualResetEventSlim.cs | 4 ++-- .../System.Private.CoreLib/src/System/Threading/SpinLock.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs index fcf7cc093f21b1..54b6596d6a4c94 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @@ -508,7 +508,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) // We spin briefly before falling back to allocating and/or waiting on a true event. - uint startTime = 0; + long startTime = 0; bool bNeedTimeoutAdjustment = false; int realMillisecondsTimeout = millisecondsTimeout; // this will be adjusted if necessary. @@ -520,7 +520,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) // period of time. The timeout adjustments only take effect when and if we actually // decide to block in the kernel below. - startTime = (uint)Environment.TickCount; + startTime = Environment.TickCount64; bNeedTimeoutAdjustment = true; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs index a199479785b3da..d50709ce2e5fc5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs @@ -287,10 +287,10 @@ private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken) nameof(millisecondsTimeout), millisecondsTimeout, SR.SpinLock_TryEnter_ArgumentOutOfRange); } - uint startTime = 0; + long startTime = 0; if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0) { - startTime = (uint)Environment.TickCount; + startTime = Environment.TickCount64; } if (IsThreadOwnerTrackingEnabled) @@ -404,7 +404,7 @@ private void DecrementWaiters() /// /// ContinueTryEnter for the thread tracking mode enabled /// - private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, uint startTime, ref bool lockTaken) + private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, long startTime, ref bool lockTaken) { Debug.Assert(IsThreadOwnerTrackingEnabled); From e64966b045c159b4d33ab0c7233e664a99363126 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:54:55 -0700 Subject: [PATCH 12/13] Update src/libraries/System.Threading/tests/SemaphoreSlimTests.cs Co-authored-by: Jan Kotas --- src/libraries/System.Threading/tests/SemaphoreSlimTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index c7b1a5447feead..7bdc0fdb60500a 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -87,6 +87,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, 10, true, null); RunSemaphoreSlimTest1_WaitAsync_Helper(0, 10, 10, false, null); RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.FromMilliseconds(uint.MaxValue), true, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.MaxValue, true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] From 47c57f20d2ea024e22c0674b98f3e8739b128f98 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Fri, 1 Aug 2025 16:02:36 -0700 Subject: [PATCH 13/13] Rename methods to WaitCore/WaitAsyncCore --- .../src/System/Threading/SemaphoreSlim.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index 993a1b95882ad6..bae72e8311816b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -178,7 +178,7 @@ public void Wait() if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.Infinite, CancellationToken.None); + WaitCore(Timeout.Infinite, CancellationToken.None); } /// @@ -198,7 +198,7 @@ public void Wait(CancellationToken cancellationToken) if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif // Call wait with infinite timeout - Wait(Timeout.Infinite, cancellationToken); + WaitCore(Timeout.Infinite, cancellationToken); } /// @@ -227,7 +227,7 @@ public bool Wait(TimeSpan timeout) } // Call wait with the timeout milliseconds - return Wait(totalMilliseconds, CancellationToken.None); + return WaitCore(totalMilliseconds, CancellationToken.None); } /// @@ -260,7 +260,7 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken) } // Call wait with the timeout milliseconds - return Wait(totalMilliseconds, cancellationToken); + return WaitCore(totalMilliseconds, cancellationToken); } /// @@ -279,7 +279,7 @@ public bool Wait(int millisecondsTimeout) #if TARGET_WASI if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185 #endif - return Wait(millisecondsTimeout, CancellationToken.None); + return WaitCore(millisecondsTimeout, CancellationToken.None); } /// @@ -307,7 +307,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } - return Wait(millisecondsTimeout, cancellationToken); + return WaitCore(millisecondsTimeout, cancellationToken); } /// @@ -321,7 +321,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) /// true if the current thread successfully entered the ; otherwise, false. /// was canceled. [UnsupportedOSPlatform("browser")] - private bool Wait(long millisecondsTimeout, CancellationToken cancellationToken) + private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose(); #if FEATURE_WASM_MANAGED_THREADS @@ -382,7 +382,7 @@ private bool Wait(long millisecondsTimeout, CancellationToken cancellationToken) if (m_asyncHead is not null) { Debug.Assert(m_asyncTail is not null, "tail should not be null if head isn't"); - asyncWaitTask = WaitAsync(millisecondsTimeout, cancellationToken); + asyncWaitTask = WaitAsyncCore(millisecondsTimeout, cancellationToken); } // There are no async waiters, so we can proceed with normal synchronous waiting. else @@ -529,7 +529,7 @@ private bool WaitUntilCountOrTimeout(long millisecondsTimeout, long startTime, C /// A task that will complete when the semaphore has been entered. public Task WaitAsync() { - return WaitAsync(Timeout.Infinite, default); + return WaitAsyncCore(Timeout.Infinite, default); } /// @@ -545,7 +545,7 @@ public Task WaitAsync() /// public Task WaitAsync(CancellationToken cancellationToken) { - return WaitAsync(Timeout.Infinite, cancellationToken); + return WaitAsyncCore(Timeout.Infinite, cancellationToken); } /// @@ -566,7 +566,7 @@ public Task WaitAsync(CancellationToken cancellationToken) /// public Task WaitAsync(int millisecondsTimeout) { - return WaitAsync(millisecondsTimeout, default); + return WaitAsyncCore(millisecondsTimeout, default); } /// @@ -591,7 +591,16 @@ public Task WaitAsync(int millisecondsTimeout) /// public Task WaitAsync(TimeSpan timeout) { - return WaitAsync(timeout, default); + // Validate the timeout + long totalMilliseconds = (long)timeout.TotalMilliseconds; + if (totalMilliseconds < -1) + { + throw new ArgumentOutOfRangeException( + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); + } + + // Call wait with the timeout milliseconds + return WaitAsyncCore(totalMilliseconds, default); } /// @@ -624,7 +633,7 @@ public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToke } // Call wait with the timeout milliseconds - return WaitAsync(totalMilliseconds, cancellationToken); + return WaitAsyncCore(totalMilliseconds, cancellationToken); } /// @@ -653,7 +662,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } - return WaitAsync((long)millisecondsTimeout, cancellationToken); + return WaitAsyncCore(millisecondsTimeout, cancellationToken); } /// @@ -671,7 +680,7 @@ public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellat /// /// The current instance has already been /// disposed. - private Task WaitAsync(long millisecondsTimeout, CancellationToken cancellationToken) + private Task WaitAsyncCore(long millisecondsTimeout, CancellationToken cancellationToken) { CheckDispose();