diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 497a524ed303fa..e5d98648641517 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 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/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs index 78f83a11bf66b0..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 = TimeoutHelper.GetTime(); + startTime = Environment.TickCount64; 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 bab6bbd332b19a..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); } /// @@ -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 . + /// 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 > int.MaxValue) + if (totalMilliseconds < -1) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return Wait((int)timeout.TotalMilliseconds, CancellationToken.None); + return WaitCore(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 . + /// 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 > int.MaxValue) + if (totalMilliseconds < -1) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return Wait((int)timeout.TotalMilliseconds, cancellationToken); + return WaitCore(totalMilliseconds, cancellationToken); } /// @@ -281,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); } /// @@ -302,10 +300,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 +307,26 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong); } + return WaitCore(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 WaitCore(long 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 @@ -322,10 +336,10 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) return false; } - uint startTime = 0; + long startTime = 0; if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0) { - startTime = TimeoutHelper.GetTime(); + startTime = Environment.TickCount64; } bool waitSuccessful = false; @@ -368,7 +382,7 @@ public bool Wait(int 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 @@ -449,12 +463,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(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 #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 +476,32 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca // If cancelled, we throw. Trying to wait could lead to deadlock. cancellationToken.ThrowIfCancellationRequested(); + // 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.Infinite) { - remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout); + long 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 +514,7 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca --m_countOfWaitersPulsedToWake; } - if (!waitSuccessful) + if (!timeoutIsCapped && !waitSuccessful) { return false; } @@ -500,7 +529,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 WaitAsyncCore(Timeout.Infinite, default); } /// @@ -516,7 +545,7 @@ public Task WaitAsync() /// public Task WaitAsync(CancellationToken cancellationToken) { - return WaitAsync(Timeout.Infinite, cancellationToken); + return WaitAsyncCore(Timeout.Infinite, cancellationToken); } /// @@ -537,7 +566,7 @@ public Task WaitAsync(CancellationToken cancellationToken) /// public Task WaitAsync(int millisecondsTimeout) { - return WaitAsync(millisecondsTimeout, default); + return WaitAsyncCore(millisecondsTimeout, default); } /// @@ -562,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); } /// @@ -588,14 +626,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) { throw new ArgumentOutOfRangeException( - nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong); + nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong); } // Call wait with the timeout milliseconds - return WaitAsync((int)timeout.TotalMilliseconds, cancellationToken); + return WaitAsyncCore(totalMilliseconds, cancellationToken); } /// @@ -618,14 +656,34 @@ 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 WaitAsyncCore(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 WaitAsyncCore(long millisecondsTimeout, CancellationToken cancellationToken) + { + CheckDispose(); + // Bail early for cancellation if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); @@ -716,12 +774,14 @@ 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, 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), cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + await ((Task)asyncWaiter.WaitAsync( + 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..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 = TimeoutHelper.GetTime(); + 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); 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 b17d38d44d7131..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,42 +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; - } - /// /// 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, or -1 if the time expired - public static int UpdateTimeOut(uint startTime, int originalWaitMillisecondsTimeout) + /// The new wait time in milliseconds + 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; diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index faac4d9cf9f85b..7bdc0fdb60500a 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, TimeSpan.FromMilliseconds(uint.MaxValue), true, null); } [Fact] @@ -67,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, new TimeSpan(0, 0, int.MaxValue), true, typeof(ArgumentOutOfRangeException)); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -87,6 +86,8 @@ 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(uint.MaxValue), true, null); + RunSemaphoreSlimTest1_WaitAsync_Helper(1, 10, TimeSpan.MaxValue, true, null); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -95,8 +96,6 @@ 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)); RunSemaphoreSlimTest1_WaitAsync2(); }