Skip to content

Commit b267b85

Browse files
authored
Extend allowed SemaphoreSlim wait TimeSpan values to match Timer and other APIs (#117398)
1 parent cc1493b commit b267b85

File tree

7 files changed

+118
-68
lines changed

7 files changed

+118
-68
lines changed

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3413,6 +3413,9 @@
34133413
<data name="SemaphoreSlim_Wait_TimeoutWrong" xml:space="preserve">
34143414
<value>The timeout must represent a value between -1 and Int32.MaxValue, inclusive.</value>
34153415
</data>
3416+
<data name="SemaphoreSlim_Wait_TimeSpanTimeoutWrong" xml:space="preserve">
3417+
<value>The value needs to translate in milliseconds to -1 (signifying an infinite timeout), or be non-negative.</value>
3418+
</data>
34163419
<data name="Serialization_BadParameterInfo" xml:space="preserve">
34173420
<value>Non existent ParameterInfo. Position bigger than member's parameters length.</value>
34183421
</data>

src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
508508

509509

510510
// We spin briefly before falling back to allocating and/or waiting on a true event.
511-
uint startTime = 0;
511+
long startTime = 0;
512512
bool bNeedTimeoutAdjustment = false;
513513
int realMillisecondsTimeout = millisecondsTimeout; // this will be adjusted if necessary.
514514

@@ -520,7 +520,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
520520
// period of time. The timeout adjustments only take effect when and if we actually
521521
// decide to block in the kernel below.
522522

523-
startTime = TimeoutHelper.GetTime();
523+
startTime = Environment.TickCount64;
524524
bNeedTimeoutAdjustment = true;
525525
}
526526

@@ -558,7 +558,8 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
558558
// update timeout (delays in wait commencement are due to spinning and/or spurious wakeups from other waits being canceled)
559559
if (bNeedTimeoutAdjustment)
560560
{
561-
realMillisecondsTimeout = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
561+
// TimeoutHelper.UpdateTimeOut returns a long but the value is capped as millisecondsTimeout is an int.
562+
realMillisecondsTimeout = (int)TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
562563
if (realMillisecondsTimeout <= 0)
563564
return false;
564565
}

src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void Wait()
178178
if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185
179179
#endif
180180
// Call wait with infinite timeout
181-
Wait(Timeout.Infinite, CancellationToken.None);
181+
WaitCore(Timeout.Infinite, CancellationToken.None);
182182
}
183183

184184
/// <summary>
@@ -198,7 +198,7 @@ public void Wait(CancellationToken cancellationToken)
198198
if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185
199199
#endif
200200
// Call wait with infinite timeout
201-
Wait(Timeout.Infinite, cancellationToken);
201+
WaitCore(Timeout.Infinite, cancellationToken);
202202
}
203203

204204
/// <summary>
@@ -211,8 +211,7 @@ public void Wait(CancellationToken cancellationToken)
211211
/// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
212212
/// otherwise, false.</returns>
213213
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is a negative
214-
/// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
215-
/// than <see cref="int.MaxValue"/>.</exception>
214+
/// number other than -1 milliseconds, which represents an infinite time-out.</exception>
216215
[UnsupportedOSPlatform("browser")]
217216
public bool Wait(TimeSpan timeout)
218217
{
@@ -221,14 +220,14 @@ public bool Wait(TimeSpan timeout)
221220
#endif
222221
// Validate the timeout
223222
long totalMilliseconds = (long)timeout.TotalMilliseconds;
224-
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
223+
if (totalMilliseconds < -1)
225224
{
226225
throw new ArgumentOutOfRangeException(
227-
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
226+
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong);
228227
}
229228

230229
// Call wait with the timeout milliseconds
231-
return Wait((int)timeout.TotalMilliseconds, CancellationToken.None);
230+
return WaitCore(totalMilliseconds, CancellationToken.None);
232231
}
233232

234233
/// <summary>
@@ -244,8 +243,7 @@ public bool Wait(TimeSpan timeout)
244243
/// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
245244
/// otherwise, false.</returns>
246245
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is a negative
247-
/// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
248-
/// than <see cref="int.MaxValue"/>.</exception>
246+
/// number other than -1 milliseconds, which represents an infinite time-out.</exception>
249247
/// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
250248
[UnsupportedOSPlatform("browser")]
251249
public bool Wait(TimeSpan timeout, CancellationToken cancellationToken)
@@ -255,14 +253,14 @@ public bool Wait(TimeSpan timeout, CancellationToken cancellationToken)
255253
#endif
256254
// Validate the timeout
257255
long totalMilliseconds = (long)timeout.TotalMilliseconds;
258-
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
256+
if (totalMilliseconds < -1)
259257
{
260258
throw new ArgumentOutOfRangeException(
261-
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
259+
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong);
262260
}
263261

264262
// Call wait with the timeout milliseconds
265-
return Wait((int)timeout.TotalMilliseconds, cancellationToken);
263+
return WaitCore(totalMilliseconds, cancellationToken);
266264
}
267265

268266
/// <summary>
@@ -281,7 +279,7 @@ public bool Wait(int millisecondsTimeout)
281279
#if TARGET_WASI
282280
if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185
283281
#endif
284-
return Wait(millisecondsTimeout, CancellationToken.None);
282+
return WaitCore(millisecondsTimeout, CancellationToken.None);
285283
}
286284

287285
/// <summary>
@@ -302,17 +300,33 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
302300
#if TARGET_WASI
303301
if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185
304302
#endif
305-
CheckDispose();
306-
#if FEATURE_WASM_MANAGED_THREADS
307-
Thread.AssureBlockingPossible();
308-
#endif
309303

310304
if (millisecondsTimeout < -1)
311305
{
312306
throw new ArgumentOutOfRangeException(
313307
nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
314308
}
315309

310+
return WaitCore(millisecondsTimeout, cancellationToken);
311+
}
312+
313+
/// <summary>
314+
/// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
315+
/// using a 32-bit unsigned integer to measure the time interval,
316+
/// while observing a <see cref="CancellationToken"/>.
317+
/// </summary>
318+
/// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.UnsignedInfinite"/> to
319+
/// wait indefinitely.</param>
320+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
321+
/// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>; otherwise, false.</returns>
322+
/// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
323+
[UnsupportedOSPlatform("browser")]
324+
private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationToken)
325+
{
326+
CheckDispose();
327+
#if FEATURE_WASM_MANAGED_THREADS
328+
Thread.AssureBlockingPossible();
329+
#endif
316330
cancellationToken.ThrowIfCancellationRequested();
317331

318332
// Perf: Check the stack timeout parameter before checking the volatile count
@@ -322,10 +336,10 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
322336
return false;
323337
}
324338

325-
uint startTime = 0;
339+
long startTime = 0;
326340
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
327341
{
328-
startTime = TimeoutHelper.GetTime();
342+
startTime = Environment.TickCount64;
329343
}
330344

331345
bool waitSuccessful = false;
@@ -368,7 +382,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
368382
if (m_asyncHead is not null)
369383
{
370384
Debug.Assert(m_asyncTail is not null, "tail should not be null if head isn't");
371-
asyncWaitTask = WaitAsync(millisecondsTimeout, cancellationToken);
385+
asyncWaitTask = WaitAsyncCore(millisecondsTimeout, cancellationToken);
372386
}
373387
// There are no async waiters, so we can proceed with normal synchronous waiting.
374388
else
@@ -449,30 +463,45 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
449463
/// <param name="cancellationToken">The CancellationToken to observe.</param>
450464
/// <returns>true if the monitor received a signal, false if the timeout expired</returns>
451465
[UnsupportedOSPlatform("browser")]
452-
private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, CancellationToken cancellationToken)
466+
private bool WaitUntilCountOrTimeout(long millisecondsTimeout, long startTime, CancellationToken cancellationToken)
453467
{
454468
#if TARGET_WASI
455469
if (OperatingSystem.IsWasi()) throw new PlatformNotSupportedException(); // TODO remove with https://github.com/dotnet/runtime/pull/107185
456470
#endif
457-
int remainingWaitMilliseconds = Timeout.Infinite;
471+
int monitorWaitMilliseconds = Timeout.Infinite;
458472

459473
// Wait on the monitor as long as the count is zero
460474
while (m_currentCount == 0)
461475
{
462476
// If cancelled, we throw. Trying to wait could lead to deadlock.
463477
cancellationToken.ThrowIfCancellationRequested();
464478

479+
// Since Monitor.Wait will handle the actual wait and it accepts an int timeout,
480+
// we may need to cap the timeout to int.MaxValue.
481+
bool timeoutIsCapped = false;
465482
if (millisecondsTimeout != Timeout.Infinite)
466483
{
467-
remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
484+
long remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
468485
if (remainingWaitMilliseconds <= 0)
469486
{
470487
// The thread has expires its timeout
471488
return false;
472489
}
490+
if (remainingWaitMilliseconds <= int.MaxValue)
491+
{
492+
monitorWaitMilliseconds = (int)remainingWaitMilliseconds;
493+
}
494+
else
495+
{
496+
timeoutIsCapped = true;
497+
monitorWaitMilliseconds = int.MaxValue;
498+
}
473499
}
474-
// ** the actual wait **
475-
bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, remainingWaitMilliseconds);
500+
501+
502+
// The actual wait. If the timeout was capped and waitSuccessful is false, it doesn't imply
503+
// a timeout, we are just limited by Monitor.Wait's maximum timeout value.
504+
bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, monitorWaitMilliseconds);
476505

477506
// This waiter has woken up and this needs to be reflected in the count of waiters pulsed to wake. Since we
478507
// 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
485514
--m_countOfWaitersPulsedToWake;
486515
}
487516

488-
if (!waitSuccessful)
517+
if (!timeoutIsCapped && !waitSuccessful)
489518
{
490519
return false;
491520
}
@@ -500,7 +529,7 @@ private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, Ca
500529
/// <returns>A task that will complete when the semaphore has been entered.</returns>
501530
public Task WaitAsync()
502531
{
503-
return WaitAsync(Timeout.Infinite, default);
532+
return WaitAsyncCore(Timeout.Infinite, default);
504533
}
505534

506535
/// <summary>
@@ -516,7 +545,7 @@ public Task WaitAsync()
516545
/// </exception>
517546
public Task WaitAsync(CancellationToken cancellationToken)
518547
{
519-
return WaitAsync(Timeout.Infinite, cancellationToken);
548+
return WaitAsyncCore(Timeout.Infinite, cancellationToken);
520549
}
521550

522551
/// <summary>
@@ -537,7 +566,7 @@ public Task WaitAsync(CancellationToken cancellationToken)
537566
/// </exception>
538567
public Task<bool> WaitAsync(int millisecondsTimeout)
539568
{
540-
return WaitAsync(millisecondsTimeout, default);
569+
return WaitAsyncCore(millisecondsTimeout, default);
541570
}
542571

543572
/// <summary>
@@ -562,7 +591,16 @@ public Task<bool> WaitAsync(int millisecondsTimeout)
562591
/// </exception>
563592
public Task<bool> WaitAsync(TimeSpan timeout)
564593
{
565-
return WaitAsync(timeout, default);
594+
// Validate the timeout
595+
long totalMilliseconds = (long)timeout.TotalMilliseconds;
596+
if (totalMilliseconds < -1)
597+
{
598+
throw new ArgumentOutOfRangeException(
599+
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong);
600+
}
601+
602+
// Call wait with the timeout milliseconds
603+
return WaitAsyncCore(totalMilliseconds, default);
566604
}
567605

568606
/// <summary>
@@ -588,14 +626,14 @@ public Task<bool> WaitAsync(TimeSpan timeout, CancellationToken cancellationToke
588626
{
589627
// Validate the timeout
590628
long totalMilliseconds = (long)timeout.TotalMilliseconds;
591-
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
629+
if (totalMilliseconds < -1)
592630
{
593631
throw new ArgumentOutOfRangeException(
594-
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
632+
nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeSpanTimeoutWrong);
595633
}
596634

597635
// Call wait with the timeout milliseconds
598-
return WaitAsync((int)timeout.TotalMilliseconds, cancellationToken);
636+
return WaitAsyncCore(totalMilliseconds, cancellationToken);
599637
}
600638

601639
/// <summary>
@@ -618,14 +656,34 @@ public Task<bool> WaitAsync(TimeSpan timeout, CancellationToken cancellationToke
618656
/// </exception>
619657
public Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken)
620658
{
621-
CheckDispose();
622-
623659
if (millisecondsTimeout < -1)
624660
{
625661
throw new ArgumentOutOfRangeException(
626662
nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
627663
}
628664

665+
return WaitAsyncCore(millisecondsTimeout, cancellationToken);
666+
}
667+
668+
/// <summary>
669+
/// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
670+
/// using a 32-bit unsigned integer to measure the time interval,
671+
/// while observing a <see cref="CancellationToken"/>.
672+
/// </summary>
673+
/// <param name="millisecondsTimeout">
674+
/// The number of milliseconds to wait, or <see cref="Timeout.UnsignedInfinite"/> to wait indefinitely.
675+
/// </param>
676+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
677+
/// <returns>
678+
/// A task that will complete with a result of true if the current thread successfully entered
679+
/// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
680+
/// </returns>
681+
/// <exception cref="ObjectDisposedException">The current instance has already been
682+
/// disposed.</exception>
683+
private Task<bool> WaitAsyncCore(long millisecondsTimeout, CancellationToken cancellationToken)
684+
{
685+
CheckDispose();
686+
629687
// Bail early for cancellation
630688
if (cancellationToken.IsCancellationRequested)
631689
return Task.FromCanceled<bool>(cancellationToken);
@@ -716,12 +774,14 @@ private bool RemoveAsyncWaiter(TaskNode task)
716774
/// <param name="millisecondsTimeout">The timeout.</param>
717775
/// <param name="cancellationToken">The cancellation token.</param>
718776
/// <returns>The task to return to the caller.</returns>
719-
private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
777+
private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long millisecondsTimeout, CancellationToken cancellationToken)
720778
{
721779
Debug.Assert(asyncWaiter is not null, "Waiter should have been constructed");
722780
Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
723781

724-
await ((Task)asyncWaiter.WaitAsync(TimeSpan.FromMilliseconds(millisecondsTimeout), cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
782+
await ((Task)asyncWaiter.WaitAsync(
783+
TimeSpan.FromMilliseconds(millisecondsTimeout),
784+
cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
725785

726786
if (cancellationToken.IsCancellationRequested)
727787
{

src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,10 @@ private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken)
287287
nameof(millisecondsTimeout), millisecondsTimeout, SR.SpinLock_TryEnter_ArgumentOutOfRange);
288288
}
289289

290-
uint startTime = 0;
290+
long startTime = 0;
291291
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0)
292292
{
293-
startTime = TimeoutHelper.GetTime();
293+
startTime = Environment.TickCount64;
294294
}
295295

296296
if (IsThreadOwnerTrackingEnabled)
@@ -404,7 +404,7 @@ private void DecrementWaiters()
404404
/// <summary>
405405
/// ContinueTryEnter for the thread tracking mode enabled
406406
/// </summary>
407-
private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, uint startTime, ref bool lockTaken)
407+
private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, long startTime, ref bool lockTaken)
408408
{
409409
Debug.Assert(IsThreadOwnerTrackingEnabled);
410410

0 commit comments

Comments
 (0)