Fix pragma warning restore (dotnet/coreclr#26389)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Threading / SemaphoreSlim.cs
blobfcb645130dc131834e0fc3b8037f869a9531f379
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
6 using System.Runtime.CompilerServices;
7 using System.Threading.Tasks;
9 namespace System.Threading
11 /// <summary>
12 /// Limits the number of threads that can access a resource or pool of resources concurrently.
13 /// </summary>
14 /// <remarks>
15 /// <para>
16 /// The <see cref="SemaphoreSlim"/> provides a lightweight semaphore class that doesn't
17 /// use Windows kernel semaphores.
18 /// </para>
19 /// <para>
20 /// All public and protected members of <see cref="SemaphoreSlim"/> are thread-safe and may be used
21 /// concurrently from multiple threads, with the exception of Dispose, which
22 /// must only be used when all other operations on the <see cref="SemaphoreSlim"/> have
23 /// completed.
24 /// </para>
25 /// </remarks>
26 [DebuggerDisplay("Current Count = {m_currentCount}")]
27 public class SemaphoreSlim : IDisposable
29 #region Private Fields
31 // The semaphore count, initialized in the constructor to the initial value, every release call incremetns it
32 // and every wait call decrements it as long as its value is positive otherwise the wait will block.
33 // Its value must be between the maximum semaphore value and zero
34 private volatile int m_currentCount;
36 // The maximum semaphore value, it is initialized to Int.MaxValue if the client didn't specify it. it is used
37 // to check if the count excceeded the maxi value or not.
38 private readonly int m_maxCount;
40 // The number of synchronously waiting threads, it is set to zero in the constructor and increments before blocking the
41 // threading and decrements it back after that. It is used as flag for the release call to know if there are
42 // waiting threads in the monitor or not.
43 private int m_waitCount;
45 /// <summary>
46 /// This is used to help prevent waking more waiters than necessary. It's not perfect and sometimes more waiters than
47 /// necessary may still be woken, see <see cref="WaitUntilCountOrTimeout"/>.
48 /// </summary>
49 private int m_countOfWaitersPulsedToWake;
51 // Object used to synchronize access to state on the instance. The contained
52 // Boolean value indicates whether the instance has been disposed.
53 private readonly StrongBox<bool> m_lockObjAndDisposed;
55 // Act as the semaphore wait handle, it's lazily initialized if needed, the first WaitHandle call initialize it
56 // and wait an release sets and resets it respectively as long as it is not null
57 private volatile ManualResetEvent? m_waitHandle;
59 // Head of list representing asynchronous waits on the semaphore.
60 private TaskNode? m_asyncHead;
62 // Tail of list representing asynchronous waits on the semaphore.
63 private TaskNode? m_asyncTail;
65 // A pre-completed task with Result==true
66 private static readonly Task<bool> s_trueTask =
67 new Task<bool>(false, true, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default);
68 // A pre-completed task with Result==false
69 private static readonly Task<bool> s_falseTask =
70 new Task<bool>(false, false, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default);
72 // No maximum constant
73 private const int NO_MAXIMUM = int.MaxValue;
75 // Task in a linked list of asynchronous waiters
76 private sealed class TaskNode : Task<bool>
78 internal TaskNode? Prev, Next;
79 internal TaskNode() : base((object?)null, TaskCreationOptions.RunContinuationsAsynchronously) { }
81 #endregion
83 #region Public properties
85 /// <summary>
86 /// Gets the current count of the <see cref="SemaphoreSlim"/>.
87 /// </summary>
88 /// <value>The current count of the <see cref="SemaphoreSlim"/>.</value>
89 public int CurrentCount => m_currentCount;
91 /// <summary>
92 /// Returns a <see cref="System.Threading.WaitHandle"/> that can be used to wait on the semaphore.
93 /// </summary>
94 /// <value>A <see cref="System.Threading.WaitHandle"/> that can be used to wait on the
95 /// semaphore.</value>
96 /// <remarks>
97 /// A successful wait on the <see cref="AvailableWaitHandle"/> does not imply a successful wait on
98 /// the <see cref="SemaphoreSlim"/> itself, nor does it decrement the semaphore's
99 /// count. <see cref="AvailableWaitHandle"/> exists to allow a thread to block waiting on multiple
100 /// semaphores, but such a wait should be followed by a true wait on the target semaphore.
101 /// </remarks>
102 /// <exception cref="System.ObjectDisposedException">The <see
103 /// cref="SemaphoreSlim"/> has been disposed.</exception>
104 public WaitHandle AvailableWaitHandle
108 CheckDispose();
110 // Return it directly if it is not null
111 if (m_waitHandle != null)
112 return m_waitHandle;
114 // lock the count to avoid multiple threads initializing the handle if it is null
115 lock (m_lockObjAndDisposed)
117 if (m_waitHandle == null)
119 // The initial state for the wait handle is true if the count is greater than zero
120 // false otherwise
121 m_waitHandle = new ManualResetEvent(m_currentCount != 0);
124 return m_waitHandle;
128 #endregion
130 #region Constructors
131 /// <summary>
132 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
133 /// the initial number of requests that can be granted concurrently.
134 /// </summary>
135 /// <param name="initialCount">The initial number of requests for the semaphore that can be granted
136 /// concurrently.</param>
137 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="initialCount"/>
138 /// is less than 0.</exception>
139 public SemaphoreSlim(int initialCount)
140 : this(initialCount, NO_MAXIMUM)
144 /// <summary>
145 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
146 /// the initial and maximum number of requests that can be granted concurrently.
147 /// </summary>
148 /// <param name="initialCount">The initial number of requests for the semaphore that can be granted
149 /// concurrently.</param>
150 /// <param name="maxCount">The maximum number of requests for the semaphore that can be granted
151 /// concurrently.</param>
152 /// <exception cref="System.ArgumentOutOfRangeException"> <paramref name="initialCount"/>
153 /// is less than 0. -or-
154 /// <paramref name="initialCount"/> is greater than <paramref name="maxCount"/>. -or-
155 /// <paramref name="maxCount"/> is less than 0.</exception>
156 public SemaphoreSlim(int initialCount, int maxCount)
158 if (initialCount < 0 || initialCount > maxCount)
160 throw new ArgumentOutOfRangeException(
161 nameof(initialCount), initialCount, SR.SemaphoreSlim_ctor_InitialCountWrong);
164 // validate input
165 if (maxCount <= 0)
167 throw new ArgumentOutOfRangeException(nameof(maxCount), maxCount, SR.SemaphoreSlim_ctor_MaxCountWrong);
170 m_maxCount = maxCount;
171 m_currentCount = initialCount;
172 m_lockObjAndDisposed = new StrongBox<bool>();
175 #endregion
177 #region Methods
178 /// <summary>
179 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>.
180 /// </summary>
181 /// <exception cref="System.ObjectDisposedException">The current instance has already been
182 /// disposed.</exception>
183 public void Wait()
185 // Call wait with infinite timeout
186 Wait(Timeout.Infinite, new CancellationToken());
189 /// <summary>
190 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, while observing a
191 /// <see cref="System.Threading.CancellationToken"/>.
192 /// </summary>
193 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to
194 /// observe.</param>
195 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was
196 /// canceled.</exception>
197 /// <exception cref="System.ObjectDisposedException">The current instance has already been
198 /// disposed.</exception>
199 public void Wait(CancellationToken cancellationToken)
201 // Call wait with infinite timeout
202 Wait(Timeout.Infinite, cancellationToken);
205 /// <summary>
206 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a <see
207 /// cref="System.TimeSpan"/> to measure the time interval.
208 /// </summary>
209 /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds
210 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
211 /// </param>
212 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
213 /// otherwise, false.</returns>
214 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="timeout"/> is a negative
215 /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
216 /// than <see cref="int.MaxValue"/>.</exception>
217 public bool Wait(TimeSpan timeout)
219 // Validate the timeout
220 long totalMilliseconds = (long)timeout.TotalMilliseconds;
221 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
223 throw new System.ArgumentOutOfRangeException(
224 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
227 // Call wait with the timeout milliseconds
228 return Wait((int)timeout.TotalMilliseconds, new CancellationToken());
231 /// <summary>
232 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a <see
233 /// cref="System.TimeSpan"/> to measure the time interval, while observing a <see
234 /// cref="System.Threading.CancellationToken"/>.
235 /// </summary>
236 /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds
237 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
238 /// </param>
239 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to
240 /// observe.</param>
241 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
242 /// otherwise, false.</returns>
243 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="timeout"/> is a negative
244 /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
245 /// than <see cref="int.MaxValue"/>.</exception>
246 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
247 public bool Wait(TimeSpan timeout, CancellationToken cancellationToken)
249 // Validate the timeout
250 long totalMilliseconds = (long)timeout.TotalMilliseconds;
251 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
253 throw new System.ArgumentOutOfRangeException(
254 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
257 // Call wait with the timeout milliseconds
258 return Wait((int)timeout.TotalMilliseconds, cancellationToken);
261 /// <summary>
262 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a 32-bit
263 /// signed integer to measure the time interval.
264 /// </summary>
265 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see
266 /// cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param>
267 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
268 /// otherwise, false.</returns>
269 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a
270 /// negative number other than -1, which represents an infinite time-out.</exception>
271 public bool Wait(int millisecondsTimeout)
273 return Wait(millisecondsTimeout, new CancellationToken());
277 /// <summary>
278 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
279 /// using a 32-bit signed integer to measure the time interval,
280 /// while observing a <see cref="System.Threading.CancellationToken"/>.
281 /// </summary>
282 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to
283 /// wait indefinitely.</param>
284 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
285 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>; otherwise, false.</returns>
286 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
287 /// which represents an infinite time-out.</exception>
288 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
289 public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
291 CheckDispose();
293 // Validate input
294 if (millisecondsTimeout < -1)
296 throw new ArgumentOutOfRangeException(
297 nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
300 cancellationToken.ThrowIfCancellationRequested();
302 // Perf: Check the stack timeout parameter before checking the volatile count
303 if (millisecondsTimeout == 0 && m_currentCount == 0)
305 // Pessimistic fail fast, check volatile count outside lock (only when timeout is zero!)
306 return false;
309 uint startTime = 0;
310 if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
312 startTime = TimeoutHelper.GetTime();
315 bool waitSuccessful = false;
316 Task<bool>? asyncWaitTask = null;
317 bool lockTaken = false;
319 // Register for cancellation outside of the main lock.
320 // NOTE: Register/unregister inside the lock can deadlock as different lock acquisition orders could
321 // occur for (1)this.m_lockObjAndDisposed and (2)cts.internalLock
322 CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.UnsafeRegister(s_cancellationTokenCanceledEventHandler, this);
325 // Perf: first spin wait for the count to be positive.
326 // This additional amount of spinwaiting in addition
327 // to Monitor.Enter()'s spinwaiting has shown measurable perf gains in test scenarios.
328 if (m_currentCount == 0)
330 // Monitor.Enter followed by Monitor.Wait is much more expensive than waiting on an event as it involves another
331 // spin, contention, etc. The usual number of spin iterations that would otherwise be used here is increased to
332 // lessen that extra expense of doing a proper wait.
333 int spinCount = SpinWait.SpinCountforSpinBeforeWait * 4;
335 var spinner = new SpinWait();
336 while (spinner.Count < spinCount)
338 spinner.SpinOnce(sleep1Threshold: -1);
340 if (m_currentCount != 0)
342 break;
346 // entering the lock and incrementing waiters must not suffer a thread-abort, else we cannot
347 // clean up m_waitCount correctly, which may lead to deadlock due to non-woken waiters.
348 try { }
349 finally
351 Monitor.Enter(m_lockObjAndDisposed, ref lockTaken);
352 if (lockTaken)
354 m_waitCount++;
358 // If there are any async waiters, for fairness we'll get in line behind
359 // then by translating our synchronous wait into an asynchronous one that we
360 // then block on (once we've released the lock).
361 if (m_asyncHead != null)
363 Debug.Assert(m_asyncTail != null, "tail should not be null if head isn't");
364 asyncWaitTask = WaitAsync(millisecondsTimeout, cancellationToken);
366 // There are no async waiters, so we can proceed with normal synchronous waiting.
367 else
369 // If the count > 0 we are good to move on.
370 // If not, then wait if we were given allowed some wait duration
372 OperationCanceledException? oce = null;
374 if (m_currentCount == 0)
376 if (millisecondsTimeout == 0)
378 return false;
381 // Prepare for the main wait...
382 // wait until the count become greater than zero or the timeout is expired
385 waitSuccessful = WaitUntilCountOrTimeout(millisecondsTimeout, startTime, cancellationToken);
387 catch (OperationCanceledException e) { oce = e; }
390 // Now try to acquire. We prioritize acquisition over cancellation/timeout so that we don't
391 // lose any counts when there are asynchronous waiters in the mix. Asynchronous waiters
392 // defer to synchronous waiters in priority, which means that if it's possible an asynchronous
393 // waiter didn't get released because a synchronous waiter was present, we need to ensure
394 // that synchronous waiter succeeds so that they have a chance to release.
395 Debug.Assert(!waitSuccessful || m_currentCount > 0,
396 "If the wait was successful, there should be count available.");
397 if (m_currentCount > 0)
399 waitSuccessful = true;
400 m_currentCount--;
402 else if (oce != null)
404 throw oce;
407 // Exposing wait handle which is lazily initialized if needed
408 if (m_waitHandle != null && m_currentCount == 0)
410 m_waitHandle.Reset();
414 finally
416 // Release the lock
417 if (lockTaken)
419 m_waitCount--;
420 Monitor.Exit(m_lockObjAndDisposed);
423 // Unregister the cancellation callback.
424 cancellationTokenRegistration.Dispose();
427 // If we had to fall back to asynchronous waiting, block on it
428 // here now that we've released the lock, and return its
429 // result when available. Otherwise, this was a synchronous
430 // wait, and whether we successfully acquired the semaphore is
431 // stored in waitSuccessful.
433 return (asyncWaitTask != null) ? asyncWaitTask.GetAwaiter().GetResult() : waitSuccessful;
436 /// <summary>
437 /// Local helper function, waits on the monitor until the monitor receives signal or the
438 /// timeout is expired
439 /// </summary>
440 /// <param name="millisecondsTimeout">The maximum timeout</param>
441 /// <param name="startTime">The start ticks to calculate the elapsed time</param>
442 /// <param name="cancellationToken">The CancellationToken to observe.</param>
443 /// <returns>true if the monitor received a signal, false if the timeout expired</returns>
444 private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, CancellationToken cancellationToken)
446 int remainingWaitMilliseconds = Timeout.Infinite;
448 // Wait on the monitor as long as the count is zero
449 while (m_currentCount == 0)
451 // If cancelled, we throw. Trying to wait could lead to deadlock.
452 cancellationToken.ThrowIfCancellationRequested();
454 if (millisecondsTimeout != Timeout.Infinite)
456 remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
457 if (remainingWaitMilliseconds <= 0)
459 // The thread has expires its timeout
460 return false;
463 // ** the actual wait **
464 bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, remainingWaitMilliseconds);
466 // This waiter has woken up and this needs to be reflected in the count of waiters pulsed to wake. Since we
467 // don't have thread-specific pulse state, there is not enough information to tell whether this thread woke up
468 // because it was pulsed. For instance, this thread may have timed out and may have been waiting to reacquire
469 // the lock before returning from Monitor.Wait, in which case we don't know whether this thread got pulsed. So
470 // in any woken case, decrement the count if possible. As such, timeouts could cause more waiters to wake than
471 // necessary.
472 if (m_countOfWaitersPulsedToWake != 0)
474 --m_countOfWaitersPulsedToWake;
477 if (!waitSuccessful)
479 return false;
483 return true;
486 /// <summary>
487 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>.
488 /// </summary>
489 /// <returns>A task that will complete when the semaphore has been entered.</returns>
490 public Task WaitAsync()
492 return WaitAsync(Timeout.Infinite, default);
495 /// <summary>
496 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, while observing a
497 /// <see cref="System.Threading.CancellationToken"/>.
498 /// </summary>
499 /// <returns>A task that will complete when the semaphore has been entered.</returns>
500 /// <param name="cancellationToken">
501 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
502 /// </param>
503 /// <exception cref="System.ObjectDisposedException">
504 /// The current instance has already been disposed.
505 /// </exception>
506 public Task WaitAsync(CancellationToken cancellationToken)
508 return WaitAsync(Timeout.Infinite, cancellationToken);
511 /// <summary>
512 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
513 /// using a 32-bit signed integer to measure the time interval.
514 /// </summary>
515 /// <param name="millisecondsTimeout">
516 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
517 /// </param>
518 /// <returns>
519 /// A task that will complete with a result of true if the current thread successfully entered
520 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
521 /// </returns>
522 /// <exception cref="System.ObjectDisposedException">The current instance has already been
523 /// disposed.</exception>
524 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
525 /// which represents an infinite time-out.
526 /// </exception>
527 public Task<bool> WaitAsync(int millisecondsTimeout)
529 return WaitAsync(millisecondsTimeout, default);
532 /// <summary>
533 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
534 /// cref="System.TimeSpan"/> to measure the time interval, while observing a
535 /// <see cref="System.Threading.CancellationToken"/>.
536 /// </summary>
537 /// <param name="timeout">
538 /// A <see cref="System.TimeSpan"/> that represents the number of milliseconds
539 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
540 /// </param>
541 /// <returns>
542 /// A task that will complete with a result of true if the current thread successfully entered
543 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
544 /// </returns>
545 /// <exception cref="System.ObjectDisposedException">
546 /// The current instance has already been disposed.
547 /// </exception>
548 /// <exception cref="System.ArgumentOutOfRangeException">
549 /// <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents
550 /// an infinite time-out -or- timeout is greater than <see cref="int.MaxValue"/>.
551 /// </exception>
552 public Task<bool> WaitAsync(TimeSpan timeout)
554 return WaitAsync(timeout, default);
557 /// <summary>
558 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
559 /// cref="System.TimeSpan"/> to measure the time interval.
560 /// </summary>
561 /// <param name="timeout">
562 /// A <see cref="System.TimeSpan"/> that represents the number of milliseconds
563 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
564 /// </param>
565 /// <param name="cancellationToken">
566 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
567 /// </param>
568 /// <returns>
569 /// A task that will complete with a result of true if the current thread successfully entered
570 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
571 /// </returns>
572 /// <exception cref="System.ArgumentOutOfRangeException">
573 /// <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents
574 /// an infinite time-out -or- timeout is greater than <see cref="int.MaxValue"/>.
575 /// </exception>
576 public Task<bool> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
578 // Validate the timeout
579 long totalMilliseconds = (long)timeout.TotalMilliseconds;
580 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
582 throw new System.ArgumentOutOfRangeException(
583 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
586 // Call wait with the timeout milliseconds
587 return WaitAsync((int)timeout.TotalMilliseconds, cancellationToken);
590 /// <summary>
591 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
592 /// using a 32-bit signed integer to measure the time interval,
593 /// while observing a <see cref="System.Threading.CancellationToken"/>.
594 /// </summary>
595 /// <param name="millisecondsTimeout">
596 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
597 /// </param>
598 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
599 /// <returns>
600 /// A task that will complete with a result of true if the current thread successfully entered
601 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
602 /// </returns>
603 /// <exception cref="System.ObjectDisposedException">The current instance has already been
604 /// disposed.</exception>
605 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
606 /// which represents an infinite time-out.
607 /// </exception>
608 public Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken)
610 CheckDispose();
612 // Validate input
613 if (millisecondsTimeout < -1)
615 throw new ArgumentOutOfRangeException(
616 nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
619 // Bail early for cancellation
620 if (cancellationToken.IsCancellationRequested)
621 return Task.FromCanceled<bool>(cancellationToken);
623 lock (m_lockObjAndDisposed)
625 // If there are counts available, allow this waiter to succeed.
626 if (m_currentCount > 0)
628 --m_currentCount;
629 if (m_waitHandle != null && m_currentCount == 0) m_waitHandle.Reset();
630 return s_trueTask;
632 else if (millisecondsTimeout == 0)
634 // No counts, if timeout is zero fail fast
635 return s_falseTask;
637 // If there aren't, create and return a task to the caller.
638 // The task will be completed either when they've successfully acquired
639 // the semaphore or when the timeout expired or cancellation was requested.
640 else
642 Debug.Assert(m_currentCount == 0, "m_currentCount should never be negative");
643 TaskNode asyncWaiter = CreateAndAddAsyncWaiter();
644 return (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled) ?
645 asyncWaiter :
646 WaitUntilCountOrTimeoutAsync(asyncWaiter, millisecondsTimeout, cancellationToken);
651 /// <summary>Creates a new task and stores it into the async waiters list.</summary>
652 /// <returns>The created task.</returns>
653 private TaskNode CreateAndAddAsyncWaiter()
655 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
657 // Create the task
658 var task = new TaskNode();
660 // Add it to the linked list
661 if (m_asyncHead == null)
663 Debug.Assert(m_asyncTail == null, "If head is null, so too should be tail");
664 m_asyncHead = task;
665 m_asyncTail = task;
667 else
669 Debug.Assert(m_asyncTail != null, "If head is not null, neither should be tail");
670 m_asyncTail.Next = task;
671 task.Prev = m_asyncTail;
672 m_asyncTail = task;
675 // Hand it back
676 return task;
679 /// <summary>Removes the waiter task from the linked list.</summary>
680 /// <param name="task">The task to remove.</param>
681 /// <returns>true if the waiter was in the list; otherwise, false.</returns>
682 private bool RemoveAsyncWaiter(TaskNode task)
684 Debug.Assert(task != null, "Expected non-null task");
685 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
687 // Is the task in the list? To be in the list, either it's the head or it has a predecessor that's in the list.
688 bool wasInList = m_asyncHead == task || task.Prev != null;
690 // Remove it from the linked list
691 if (task.Next != null) task.Next.Prev = task.Prev;
692 if (task.Prev != null) task.Prev.Next = task.Next;
693 if (m_asyncHead == task) m_asyncHead = task.Next;
694 if (m_asyncTail == task) m_asyncTail = task.Prev;
695 Debug.Assert((m_asyncHead == null) == (m_asyncTail == null), "Head is null iff tail is null");
697 // Make sure not to leak
698 task.Next = task.Prev = null;
700 // Return whether the task was in the list
701 return wasInList;
704 /// <summary>Performs the asynchronous wait.</summary>
705 /// <param name="asyncWaiter">The asynchronous waiter.</param>
706 /// <param name="millisecondsTimeout">The timeout.</param>
707 /// <param name="cancellationToken">The cancellation token.</param>
708 /// <returns>The task to return to the caller.</returns>
709 private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
711 Debug.Assert(asyncWaiter != null, "Waiter should have been constructed");
712 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
714 if (millisecondsTimeout != Timeout.Infinite)
716 // Wait until either the task is completed, cancellation is requested, or the timeout occurs.
717 // We need to ensure that the Task.Delay task is appropriately cleaned up if the await
718 // completes due to the asyncWaiter completing, so we use our own token that we can explicitly
719 // cancel, and we chain the caller's supplied token into it.
720 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default))
722 if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, Task.Delay(millisecondsTimeout, cts.Token) }).ConfigureAwait(false))
724 cts.Cancel(); // ensure that the Task.Delay task is cleaned up
725 return true; // successfully acquired
729 else // millisecondsTimeout == Timeout.Infinite
731 // Wait until either the task is completed or cancellation is requested.
732 var cancellationTask = new Task();
733 using (cancellationToken.UnsafeRegister(s => ((Task)s!).TrySetResult(), cancellationTask))
735 if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, cancellationTask }).ConfigureAwait(false))
737 return true; // successfully acquired
742 // If we get here, the wait has timed out or been canceled.
744 // If the await completed synchronously, we still hold the lock. If it didn't,
745 // we no longer hold the lock. As such, acquire it.
746 lock (m_lockObjAndDisposed)
748 // Remove the task from the list. If we're successful in doing so,
749 // we know that no one else has tried to complete this waiter yet,
750 // so we can safely cancel or timeout.
751 if (RemoveAsyncWaiter(asyncWaiter))
753 cancellationToken.ThrowIfCancellationRequested(); // cancellation occurred
754 return false; // timeout occurred
758 // The waiter had already been removed, which means it's already completed or is about to
759 // complete, so let it, and don't return until it does.
760 return await asyncWaiter.ConfigureAwait(false);
763 /// <summary>
764 /// Exits the <see cref="SemaphoreSlim"/> once.
765 /// </summary>
766 /// <returns>The previous count of the <see cref="SemaphoreSlim"/>.</returns>
767 /// <exception cref="System.ObjectDisposedException">The current instance has already been
768 /// disposed.</exception>
769 public int Release()
771 return Release(1);
774 /// <summary>
775 /// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
776 /// </summary>
777 /// <param name="releaseCount">The number of times to exit the semaphore.</param>
778 /// <returns>The previous count of the <see cref="SemaphoreSlim"/>.</returns>
779 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="releaseCount"/> is less
780 /// than 1.</exception>
781 /// <exception cref="System.Threading.SemaphoreFullException">The <see cref="SemaphoreSlim"/> has
782 /// already reached its maximum size.</exception>
783 /// <exception cref="System.ObjectDisposedException">The current instance has already been
784 /// disposed.</exception>
785 public int Release(int releaseCount)
787 CheckDispose();
789 // Validate input
790 if (releaseCount < 1)
792 throw new ArgumentOutOfRangeException(
793 nameof(releaseCount), releaseCount, SR.SemaphoreSlim_Release_CountWrong);
795 int returnCount;
797 lock (m_lockObjAndDisposed)
799 // Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
800 int currentCount = m_currentCount;
801 returnCount = currentCount;
803 // If the release count would result exceeding the maximum count, throw SemaphoreFullException.
804 if (m_maxCount - currentCount < releaseCount)
806 throw new SemaphoreFullException();
809 // Increment the count by the actual release count
810 currentCount += releaseCount;
812 // Signal to any synchronous waiters, taking into account how many waiters have previously been pulsed to wake
813 // but have not yet woken
814 int waitCount = m_waitCount;
815 Debug.Assert(m_countOfWaitersPulsedToWake <= waitCount);
816 int waitersToNotify = Math.Min(currentCount, waitCount) - m_countOfWaitersPulsedToWake;
817 if (waitersToNotify > 0)
819 // Ideally, limiting to a maximum of releaseCount would not be necessary and could be an assert instead, but
820 // since WaitUntilCountOrTimeout() does not have enough information to tell whether a woken thread was
821 // pulsed, it's possible for m_countOfWaitersPulsedToWake to be less than the number of threads that have
822 // actually been pulsed to wake.
823 if (waitersToNotify > releaseCount)
825 waitersToNotify = releaseCount;
828 m_countOfWaitersPulsedToWake += waitersToNotify;
829 for (int i = 0; i < waitersToNotify; i++)
831 Monitor.Pulse(m_lockObjAndDisposed);
835 // Now signal to any asynchronous waiters, if there are any. While we've already
836 // signaled the synchronous waiters, we still hold the lock, and thus
837 // they won't have had an opportunity to acquire this yet. So, when releasing
838 // asynchronous waiters, we assume that all synchronous waiters will eventually
839 // acquire the semaphore. That could be a faulty assumption if those synchronous
840 // waits are canceled, but the wait code path will handle that.
841 if (m_asyncHead != null)
843 Debug.Assert(m_asyncTail != null, "tail should not be null if head isn't null");
844 int maxAsyncToRelease = currentCount - waitCount;
845 while (maxAsyncToRelease > 0 && m_asyncHead != null)
847 --currentCount;
848 --maxAsyncToRelease;
850 // Get the next async waiter to release and queue it to be completed
851 TaskNode waiterTask = m_asyncHead;
852 RemoveAsyncWaiter(waiterTask); // ensures waiterTask.Next/Prev are null
853 waiterTask.TrySetResult(result: true);
856 m_currentCount = currentCount;
858 // Exposing wait handle if it is not null
859 if (m_waitHandle != null && returnCount == 0 && currentCount > 0)
861 m_waitHandle.Set();
865 // And return the count
866 return returnCount;
869 /// <summary>
870 /// Releases all resources used by the current instance of <see
871 /// cref="SemaphoreSlim"/>.
872 /// </summary>
873 /// <remarks>
874 /// Unlike most of the members of <see cref="SemaphoreSlim"/>, <see cref="Dispose()"/> is not
875 /// thread-safe and may not be used concurrently with other members of this instance.
876 /// </remarks>
877 public void Dispose()
879 Dispose(true);
880 GC.SuppressFinalize(this);
883 /// <summary>
884 /// When overridden in a derived class, releases the unmanaged resources used by the
885 /// <see cref="System.Threading.ManualResetEventSlim"/>, and optionally releases the managed resources.
886 /// </summary>
887 /// <param name="disposing">true to release both managed and unmanaged resources;
888 /// false to release only unmanaged resources.</param>
889 /// <remarks>
890 /// Unlike most of the members of <see cref="SemaphoreSlim"/>, <see cref="Dispose(bool)"/> is not
891 /// thread-safe and may not be used concurrently with other members of this instance.
892 /// </remarks>
893 protected virtual void Dispose(bool disposing)
895 if (disposing)
897 WaitHandle? wh = m_waitHandle;
898 if (wh != null)
900 wh.Dispose();
901 m_waitHandle = null;
904 m_lockObjAndDisposed.Value = true;
906 m_asyncHead = null;
907 m_asyncTail = null;
911 /// <summary>
912 /// Private helper method to wake up waiters when a cancellationToken gets canceled.
913 /// </summary>
914 private static readonly Action<object?> s_cancellationTokenCanceledEventHandler = new Action<object?>(CancellationTokenCanceledEventHandler);
915 private static void CancellationTokenCanceledEventHandler(object? obj)
917 Debug.Assert(obj is SemaphoreSlim, "Expected a SemaphoreSlim");
918 SemaphoreSlim semaphore = (SemaphoreSlim)obj;
919 lock (semaphore.m_lockObjAndDisposed)
921 Monitor.PulseAll(semaphore.m_lockObjAndDisposed); // wake up all waiters.
925 /// <summary>
926 /// Checks the dispose status by checking the lock object, if it is null means that object
927 /// has been disposed and throw ObjectDisposedException
928 /// </summary>
929 private void CheckDispose()
931 if (m_lockObjAndDisposed.Value)
933 throw new ObjectDisposedException(null, SR.SemaphoreSlim_Disposed);
936 #endregion