Fix StyleCop warning SA1121 (use built-in types)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Threading / SemaphoreSlim.cs
blob6d65aa1f029f59b3524548d199350e6264d98b96
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
91 get { return m_currentCount; }
94 /// <summary>
95 /// Returns a <see cref="System.Threading.WaitHandle"/> that can be used to wait on the semaphore.
96 /// </summary>
97 /// <value>A <see cref="System.Threading.WaitHandle"/> that can be used to wait on the
98 /// semaphore.</value>
99 /// <remarks>
100 /// A successful wait on the <see cref="AvailableWaitHandle"/> does not imply a successful wait on
101 /// the <see cref="SemaphoreSlim"/> itself, nor does it decrement the semaphore's
102 /// count. <see cref="AvailableWaitHandle"/> exists to allow a thread to block waiting on multiple
103 /// semaphores, but such a wait should be followed by a true wait on the target semaphore.
104 /// </remarks>
105 /// <exception cref="System.ObjectDisposedException">The <see
106 /// cref="SemaphoreSlim"/> has been disposed.</exception>
107 public WaitHandle AvailableWaitHandle
111 CheckDispose();
113 // Return it directly if it is not null
114 if (m_waitHandle != null)
115 return m_waitHandle;
117 //lock the count to avoid multiple threads initializing the handle if it is null
118 lock (m_lockObjAndDisposed)
120 if (m_waitHandle == null)
122 // The initial state for the wait handle is true if the count is greater than zero
123 // false otherwise
124 m_waitHandle = new ManualResetEvent(m_currentCount != 0);
127 return m_waitHandle;
131 #endregion
133 #region Constructors
134 /// <summary>
135 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
136 /// the initial number of requests that can be granted concurrently.
137 /// </summary>
138 /// <param name="initialCount">The initial number of requests for the semaphore that can be granted
139 /// concurrently.</param>
140 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="initialCount"/>
141 /// is less than 0.</exception>
142 public SemaphoreSlim(int initialCount)
143 : this(initialCount, NO_MAXIMUM)
147 /// <summary>
148 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
149 /// the initial and maximum number of requests that can be granted concurrently.
150 /// </summary>
151 /// <param name="initialCount">The initial number of requests for the semaphore that can be granted
152 /// concurrently.</param>
153 /// <param name="maxCount">The maximum number of requests for the semaphore that can be granted
154 /// concurrently.</param>
155 /// <exception cref="System.ArgumentOutOfRangeException"> <paramref name="initialCount"/>
156 /// is less than 0. -or-
157 /// <paramref name="initialCount"/> is greater than <paramref name="maxCount"/>. -or-
158 /// <paramref name="maxCount"/> is less than 0.</exception>
159 public SemaphoreSlim(int initialCount, int maxCount)
161 if (initialCount < 0 || initialCount > maxCount)
163 throw new ArgumentOutOfRangeException(
164 nameof(initialCount), initialCount, SR.SemaphoreSlim_ctor_InitialCountWrong);
167 //validate input
168 if (maxCount <= 0)
170 throw new ArgumentOutOfRangeException(nameof(maxCount), maxCount, SR.SemaphoreSlim_ctor_MaxCountWrong);
173 m_maxCount = maxCount;
174 m_currentCount = initialCount;
175 m_lockObjAndDisposed = new StrongBox<bool>();
178 #endregion
180 #region Methods
181 /// <summary>
182 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>.
183 /// </summary>
184 /// <exception cref="System.ObjectDisposedException">The current instance has already been
185 /// disposed.</exception>
186 public void Wait()
188 // Call wait with infinite timeout
189 Wait(Timeout.Infinite, new CancellationToken());
192 /// <summary>
193 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, while observing a
194 /// <see cref="System.Threading.CancellationToken"/>.
195 /// </summary>
196 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to
197 /// observe.</param>
198 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was
199 /// canceled.</exception>
200 /// <exception cref="System.ObjectDisposedException">The current instance has already been
201 /// disposed.</exception>
202 public void Wait(CancellationToken cancellationToken)
204 // Call wait with infinite timeout
205 Wait(Timeout.Infinite, cancellationToken);
208 /// <summary>
209 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a <see
210 /// cref="System.TimeSpan"/> to measure the time interval.
211 /// </summary>
212 /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds
213 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
214 /// </param>
215 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
216 /// otherwise, false.</returns>
217 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="timeout"/> is a negative
218 /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
219 /// than <see cref="int.MaxValue"/>.</exception>
220 public bool Wait(TimeSpan timeout)
222 // Validate the timeout
223 long totalMilliseconds = (long)timeout.TotalMilliseconds;
224 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
226 throw new System.ArgumentOutOfRangeException(
227 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
230 // Call wait with the timeout milliseconds
231 return Wait((int)timeout.TotalMilliseconds, new CancellationToken());
234 /// <summary>
235 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a <see
236 /// cref="System.TimeSpan"/> to measure the time interval, while observing a <see
237 /// cref="System.Threading.CancellationToken"/>.
238 /// </summary>
239 /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds
240 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
241 /// </param>
242 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to
243 /// observe.</param>
244 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
245 /// otherwise, false.</returns>
246 /// <exception cref="System.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>
249 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
250 public bool Wait(TimeSpan timeout, CancellationToken cancellationToken)
252 // Validate the timeout
253 long totalMilliseconds = (long)timeout.TotalMilliseconds;
254 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
256 throw new System.ArgumentOutOfRangeException(
257 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
260 // Call wait with the timeout milliseconds
261 return Wait((int)timeout.TotalMilliseconds, cancellationToken);
264 /// <summary>
265 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, using a 32-bit
266 /// signed integer to measure the time interval.
267 /// </summary>
268 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see
269 /// cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param>
270 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>;
271 /// otherwise, false.</returns>
272 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a
273 /// negative number other than -1, which represents an infinite time-out.</exception>
274 public bool Wait(int millisecondsTimeout)
276 return Wait(millisecondsTimeout, new CancellationToken());
280 /// <summary>
281 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
282 /// using a 32-bit signed integer to measure the time interval,
283 /// while observing a <see cref="System.Threading.CancellationToken"/>.
284 /// </summary>
285 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to
286 /// wait indefinitely.</param>
287 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
288 /// <returns>true if the current thread successfully entered the <see cref="SemaphoreSlim"/>; otherwise, false.</returns>
289 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
290 /// which represents an infinite time-out.</exception>
291 /// <exception cref="System.OperationCanceledException"><paramref name="cancellationToken"/> was canceled.</exception>
292 public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
294 CheckDispose();
296 // Validate input
297 if (millisecondsTimeout < -1)
299 throw new ArgumentOutOfRangeException(
300 nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
303 cancellationToken.ThrowIfCancellationRequested();
305 // Perf: Check the stack timeout parameter before checking the volatile count
306 if (millisecondsTimeout == 0 && m_currentCount == 0)
308 // Pessimistic fail fast, check volatile count outside lock (only when timeout is zero!)
309 return false;
312 uint startTime = 0;
313 if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
315 startTime = TimeoutHelper.GetTime();
318 bool waitSuccessful = false;
319 Task<bool>? asyncWaitTask = null;
320 bool lockTaken = false;
322 //Register for cancellation outside of the main lock.
323 //NOTE: Register/unregister inside the lock can deadlock as different lock acquisition orders could
324 // occur for (1)this.m_lockObjAndDisposed and (2)cts.internalLock
325 CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.UnsafeRegister(s_cancellationTokenCanceledEventHandler, this);
328 // Perf: first spin wait for the count to be positive.
329 // This additional amount of spinwaiting in addition
330 // to Monitor.Enter()’s spinwaiting has shown measurable perf gains in test scenarios.
331 if (m_currentCount == 0)
333 // Monitor.Enter followed by Monitor.Wait is much more expensive than waiting on an event as it involves another
334 // spin, contention, etc. The usual number of spin iterations that would otherwise be used here is increased to
335 // lessen that extra expense of doing a proper wait.
336 int spinCount = SpinWait.SpinCountforSpinBeforeWait * 4;
338 var spinner = new SpinWait();
339 while (spinner.Count < spinCount)
341 spinner.SpinOnce(sleep1Threshold: -1);
343 if (m_currentCount != 0)
345 break;
349 // entering the lock and incrementing waiters must not suffer a thread-abort, else we cannot
350 // clean up m_waitCount correctly, which may lead to deadlock due to non-woken waiters.
351 try { }
352 finally
354 Monitor.Enter(m_lockObjAndDisposed, ref lockTaken);
355 if (lockTaken)
357 m_waitCount++;
361 // If there are any async waiters, for fairness we'll get in line behind
362 // then by translating our synchronous wait into an asynchronous one that we
363 // then block on (once we've released the lock).
364 if (m_asyncHead != null)
366 Debug.Assert(m_asyncTail != null, "tail should not be null if head isn't");
367 asyncWaitTask = WaitAsync(millisecondsTimeout, cancellationToken);
369 // There are no async waiters, so we can proceed with normal synchronous waiting.
370 else
372 // If the count > 0 we are good to move on.
373 // If not, then wait if we were given allowed some wait duration
375 OperationCanceledException? oce = null;
377 if (m_currentCount == 0)
379 if (millisecondsTimeout == 0)
381 return false;
384 // Prepare for the main wait...
385 // wait until the count become greater than zero or the timeout is expired
388 waitSuccessful = WaitUntilCountOrTimeout(millisecondsTimeout, startTime, cancellationToken);
390 catch (OperationCanceledException e) { oce = e; }
393 // Now try to acquire. We prioritize acquisition over cancellation/timeout so that we don't
394 // lose any counts when there are asynchronous waiters in the mix. Asynchronous waiters
395 // defer to synchronous waiters in priority, which means that if it's possible an asynchronous
396 // waiter didn't get released because a synchronous waiter was present, we need to ensure
397 // that synchronous waiter succeeds so that they have a chance to release.
398 Debug.Assert(!waitSuccessful || m_currentCount > 0,
399 "If the wait was successful, there should be count available.");
400 if (m_currentCount > 0)
402 waitSuccessful = true;
403 m_currentCount--;
405 else if (oce != null)
407 throw oce;
410 // Exposing wait handle which is lazily initialized if needed
411 if (m_waitHandle != null && m_currentCount == 0)
413 m_waitHandle.Reset();
417 finally
419 // Release the lock
420 if (lockTaken)
422 m_waitCount--;
423 Monitor.Exit(m_lockObjAndDisposed);
426 // Unregister the cancellation callback.
427 cancellationTokenRegistration.Dispose();
430 // If we had to fall back to asynchronous waiting, block on it
431 // here now that we've released the lock, and return its
432 // result when available. Otherwise, this was a synchronous
433 // wait, and whether we successfully acquired the semaphore is
434 // stored in waitSuccessful.
436 return (asyncWaitTask != null) ? asyncWaitTask.GetAwaiter().GetResult() : waitSuccessful;
439 /// <summary>
440 /// Local helper function, waits on the monitor until the monitor receives signal or the
441 /// timeout is expired
442 /// </summary>
443 /// <param name="millisecondsTimeout">The maximum timeout</param>
444 /// <param name="startTime">The start ticks to calculate the elapsed time</param>
445 /// <param name="cancellationToken">The CancellationToken to observe.</param>
446 /// <returns>true if the monitor received a signal, false if the timeout expired</returns>
447 private bool WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, CancellationToken cancellationToken)
449 int remainingWaitMilliseconds = Timeout.Infinite;
451 //Wait on the monitor as long as the count is zero
452 while (m_currentCount == 0)
454 // If cancelled, we throw. Trying to wait could lead to deadlock.
455 cancellationToken.ThrowIfCancellationRequested();
457 if (millisecondsTimeout != Timeout.Infinite)
459 remainingWaitMilliseconds = TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
460 if (remainingWaitMilliseconds <= 0)
462 // The thread has expires its timeout
463 return false;
466 // ** the actual wait **
467 bool waitSuccessful = Monitor.Wait(m_lockObjAndDisposed, remainingWaitMilliseconds);
469 // This waiter has woken up and this needs to be reflected in the count of waiters pulsed to wake. Since we
470 // don't have thread-specific pulse state, there is not enough information to tell whether this thread woke up
471 // because it was pulsed. For instance, this thread may have timed out and may have been waiting to reacquire
472 // the lock before returning from Monitor.Wait, in which case we don't know whether this thread got pulsed. So
473 // in any woken case, decrement the count if possible. As such, timeouts could cause more waiters to wake than
474 // necessary.
475 if (m_countOfWaitersPulsedToWake != 0)
477 --m_countOfWaitersPulsedToWake;
480 if (!waitSuccessful)
482 return false;
486 return true;
489 /// <summary>
490 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>.
491 /// </summary>
492 /// <returns>A task that will complete when the semaphore has been entered.</returns>
493 public Task WaitAsync()
495 return WaitAsync(Timeout.Infinite, default);
498 /// <summary>
499 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, while observing a
500 /// <see cref="System.Threading.CancellationToken"/>.
501 /// </summary>
502 /// <returns>A task that will complete when the semaphore has been entered.</returns>
503 /// <param name="cancellationToken">
504 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
505 /// </param>
506 /// <exception cref="System.ObjectDisposedException">
507 /// The current instance has already been disposed.
508 /// </exception>
509 public Task WaitAsync(CancellationToken cancellationToken)
511 return WaitAsync(Timeout.Infinite, cancellationToken);
514 /// <summary>
515 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
516 /// using a 32-bit signed integer to measure the time interval.
517 /// </summary>
518 /// <param name="millisecondsTimeout">
519 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
520 /// </param>
521 /// <returns>
522 /// A task that will complete with a result of true if the current thread successfully entered
523 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
524 /// </returns>
525 /// <exception cref="System.ObjectDisposedException">The current instance has already been
526 /// disposed.</exception>
527 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
528 /// which represents an infinite time-out.
529 /// </exception>
530 public Task<bool> WaitAsync(int millisecondsTimeout)
532 return WaitAsync(millisecondsTimeout, default);
535 /// <summary>
536 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
537 /// cref="System.TimeSpan"/> to measure the time interval, while observing a
538 /// <see cref="System.Threading.CancellationToken"/>.
539 /// </summary>
540 /// <param name="timeout">
541 /// A <see cref="System.TimeSpan"/> that represents the number of milliseconds
542 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
543 /// </param>
544 /// <returns>
545 /// A task that will complete with a result of true if the current thread successfully entered
546 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
547 /// </returns>
548 /// <exception cref="System.ObjectDisposedException">
549 /// The current instance has already been disposed.
550 /// </exception>
551 /// <exception cref="System.ArgumentOutOfRangeException">
552 /// <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents
553 /// an infinite time-out -or- timeout is greater than <see cref="int.MaxValue"/>.
554 /// </exception>
555 public Task<bool> WaitAsync(TimeSpan timeout)
557 return WaitAsync(timeout, default);
560 /// <summary>
561 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
562 /// cref="System.TimeSpan"/> to measure the time interval.
563 /// </summary>
564 /// <param name="timeout">
565 /// A <see cref="System.TimeSpan"/> that represents the number of milliseconds
566 /// to wait, or a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely.
567 /// </param>
568 /// <param name="cancellationToken">
569 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
570 /// </param>
571 /// <returns>
572 /// A task that will complete with a result of true if the current thread successfully entered
573 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
574 /// </returns>
575 /// <exception cref="System.ArgumentOutOfRangeException">
576 /// <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents
577 /// an infinite time-out -or- timeout is greater than <see cref="int.MaxValue"/>.
578 /// </exception>
579 public Task<bool> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
581 // Validate the timeout
582 long totalMilliseconds = (long)timeout.TotalMilliseconds;
583 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
585 throw new System.ArgumentOutOfRangeException(
586 nameof(timeout), timeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
589 // Call wait with the timeout milliseconds
590 return WaitAsync((int)timeout.TotalMilliseconds, cancellationToken);
593 /// <summary>
594 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
595 /// using a 32-bit signed integer to measure the time interval,
596 /// while observing a <see cref="System.Threading.CancellationToken"/>.
597 /// </summary>
598 /// <param name="millisecondsTimeout">
599 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
600 /// </param>
601 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
602 /// <returns>
603 /// A task that will complete with a result of true if the current thread successfully entered
604 /// the <see cref="SemaphoreSlim"/>, otherwise with a result of false.
605 /// </returns>
606 /// <exception cref="System.ObjectDisposedException">The current instance has already been
607 /// disposed.</exception>
608 /// <exception cref="ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> is a negative number other than -1,
609 /// which represents an infinite time-out.
610 /// </exception>
611 public Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken)
613 CheckDispose();
615 // Validate input
616 if (millisecondsTimeout < -1)
618 throw new ArgumentOutOfRangeException(
619 nameof(millisecondsTimeout), millisecondsTimeout, SR.SemaphoreSlim_Wait_TimeoutWrong);
622 // Bail early for cancellation
623 if (cancellationToken.IsCancellationRequested)
624 return Task.FromCanceled<bool>(cancellationToken);
626 lock (m_lockObjAndDisposed)
628 // If there are counts available, allow this waiter to succeed.
629 if (m_currentCount > 0)
631 --m_currentCount;
632 if (m_waitHandle != null && m_currentCount == 0) m_waitHandle.Reset();
633 return s_trueTask;
635 else if (millisecondsTimeout == 0)
637 // No counts, if timeout is zero fail fast
638 return s_falseTask;
640 // If there aren't, create and return a task to the caller.
641 // The task will be completed either when they've successfully acquired
642 // the semaphore or when the timeout expired or cancellation was requested.
643 else
645 Debug.Assert(m_currentCount == 0, "m_currentCount should never be negative");
646 var asyncWaiter = CreateAndAddAsyncWaiter();
647 return (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled) ?
648 asyncWaiter :
649 WaitUntilCountOrTimeoutAsync(asyncWaiter, millisecondsTimeout, cancellationToken);
654 /// <summary>Creates a new task and stores it into the async waiters list.</summary>
655 /// <returns>The created task.</returns>
656 private TaskNode CreateAndAddAsyncWaiter()
658 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
660 // Create the task
661 var task = new TaskNode();
663 // Add it to the linked list
664 if (m_asyncHead == null)
666 Debug.Assert(m_asyncTail == null, "If head is null, so too should be tail");
667 m_asyncHead = task;
668 m_asyncTail = task;
670 else
672 Debug.Assert(m_asyncTail != null, "If head is not null, neither should be tail");
673 m_asyncTail.Next = task;
674 task.Prev = m_asyncTail;
675 m_asyncTail = task;
678 // Hand it back
679 return task;
682 /// <summary>Removes the waiter task from the linked list.</summary>
683 /// <param name="task">The task to remove.</param>
684 /// <returns>true if the waiter was in the list; otherwise, false.</returns>
685 private bool RemoveAsyncWaiter(TaskNode task)
687 Debug.Assert(task != null, "Expected non-null task");
688 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
690 // 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.
691 bool wasInList = m_asyncHead == task || task.Prev != null;
693 // Remove it from the linked list
694 if (task.Next != null) task.Next.Prev = task.Prev;
695 if (task.Prev != null) task.Prev.Next = task.Next;
696 if (m_asyncHead == task) m_asyncHead = task.Next;
697 if (m_asyncTail == task) m_asyncTail = task.Prev;
698 Debug.Assert((m_asyncHead == null) == (m_asyncTail == null), "Head is null iff tail is null");
700 // Make sure not to leak
701 task.Next = task.Prev = null;
703 // Return whether the task was in the list
704 return wasInList;
707 /// <summary>Performs the asynchronous wait.</summary>
708 /// <param name="asyncWaiter">The asynchronous waiter.</param>
709 /// <param name="millisecondsTimeout">The timeout.</param>
710 /// <param name="cancellationToken">The cancellation token.</param>
711 /// <returns>The task to return to the caller.</returns>
712 private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
714 Debug.Assert(asyncWaiter != null, "Waiter should have been constructed");
715 Debug.Assert(Monitor.IsEntered(m_lockObjAndDisposed), "Requires the lock be held");
717 if (millisecondsTimeout != Timeout.Infinite)
719 // Wait until either the task is completed, cancellation is requested, or the timeout occurs.
720 // We need to ensure that the Task.Delay task is appropriately cleaned up if the await
721 // completes due to the asyncWaiter completing, so we use our own token that we can explicitly
722 // cancel, and we chain the caller's supplied token into it.
723 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default))
725 if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, Task.Delay(millisecondsTimeout, cts.Token) }).ConfigureAwait(false))
727 cts.Cancel(); // ensure that the Task.Delay task is cleaned up
728 return true; // successfully acquired
732 else // millisecondsTimeout == Timeout.Infinite
734 // Wait until either the task is completed or cancellation is requested.
735 var cancellationTask = new Task();
736 using (cancellationToken.UnsafeRegister(s => ((Task)s!).TrySetResult(), cancellationTask))
738 if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, cancellationTask }).ConfigureAwait(false))
740 return true; // successfully acquired
745 // If we get here, the wait has timed out or been canceled.
747 // If the await completed synchronously, we still hold the lock. If it didn't,
748 // we no longer hold the lock. As such, acquire it.
749 lock (m_lockObjAndDisposed)
751 // Remove the task from the list. If we're successful in doing so,
752 // we know that no one else has tried to complete this waiter yet,
753 // so we can safely cancel or timeout.
754 if (RemoveAsyncWaiter(asyncWaiter))
756 cancellationToken.ThrowIfCancellationRequested(); // cancellation occurred
757 return false; // timeout occurred
761 // The waiter had already been removed, which means it's already completed or is about to
762 // complete, so let it, and don't return until it does.
763 return await asyncWaiter.ConfigureAwait(false);
766 /// <summary>
767 /// Exits the <see cref="SemaphoreSlim"/> once.
768 /// </summary>
769 /// <returns>The previous count of the <see cref="SemaphoreSlim"/>.</returns>
770 /// <exception cref="System.ObjectDisposedException">The current instance has already been
771 /// disposed.</exception>
772 public int Release()
774 return Release(1);
777 /// <summary>
778 /// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
779 /// </summary>
780 /// <param name="releaseCount">The number of times to exit the semaphore.</param>
781 /// <returns>The previous count of the <see cref="SemaphoreSlim"/>.</returns>
782 /// <exception cref="System.ArgumentOutOfRangeException"><paramref name="releaseCount"/> is less
783 /// than 1.</exception>
784 /// <exception cref="System.Threading.SemaphoreFullException">The <see cref="SemaphoreSlim"/> has
785 /// already reached its maximum size.</exception>
786 /// <exception cref="System.ObjectDisposedException">The current instance has already been
787 /// disposed.</exception>
788 public int Release(int releaseCount)
790 CheckDispose();
792 // Validate input
793 if (releaseCount < 1)
795 throw new ArgumentOutOfRangeException(
796 nameof(releaseCount), releaseCount, SR.SemaphoreSlim_Release_CountWrong);
798 int returnCount;
800 lock (m_lockObjAndDisposed)
802 // Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
803 int currentCount = m_currentCount;
804 returnCount = currentCount;
806 // If the release count would result exceeding the maximum count, throw SemaphoreFullException.
807 if (m_maxCount - currentCount < releaseCount)
809 throw new SemaphoreFullException();
812 // Increment the count by the actual release count
813 currentCount += releaseCount;
815 // Signal to any synchronous waiters, taking into account how many waiters have previously been pulsed to wake
816 // but have not yet woken
817 int waitCount = m_waitCount;
818 Debug.Assert(m_countOfWaitersPulsedToWake <= waitCount);
819 int waitersToNotify = Math.Min(currentCount, waitCount) - m_countOfWaitersPulsedToWake;
820 if (waitersToNotify > 0)
822 // Ideally, limiting to a maximum of releaseCount would not be necessary and could be an assert instead, but
823 // since WaitUntilCountOrTimeout() does not have enough information to tell whether a woken thread was
824 // pulsed, it's possible for m_countOfWaitersPulsedToWake to be less than the number of threads that have
825 // actually been pulsed to wake.
826 if (waitersToNotify > releaseCount)
828 waitersToNotify = releaseCount;
831 m_countOfWaitersPulsedToWake += waitersToNotify;
832 for (int i = 0; i < waitersToNotify; i++)
834 Monitor.Pulse(m_lockObjAndDisposed);
838 // Now signal to any asynchronous waiters, if there are any. While we've already
839 // signaled the synchronous waiters, we still hold the lock, and thus
840 // they won't have had an opportunity to acquire this yet. So, when releasing
841 // asynchronous waiters, we assume that all synchronous waiters will eventually
842 // acquire the semaphore. That could be a faulty assumption if those synchronous
843 // waits are canceled, but the wait code path will handle that.
844 if (m_asyncHead != null)
846 Debug.Assert(m_asyncTail != null, "tail should not be null if head isn't null");
847 int maxAsyncToRelease = currentCount - waitCount;
848 while (maxAsyncToRelease > 0 && m_asyncHead != null)
850 --currentCount;
851 --maxAsyncToRelease;
853 // Get the next async waiter to release and queue it to be completed
854 var waiterTask = m_asyncHead;
855 RemoveAsyncWaiter(waiterTask); // ensures waiterTask.Next/Prev are null
856 waiterTask.TrySetResult(result: true);
859 m_currentCount = currentCount;
861 // Exposing wait handle if it is not null
862 if (m_waitHandle != null && returnCount == 0 && currentCount > 0)
864 m_waitHandle.Set();
868 // And return the count
869 return returnCount;
872 /// <summary>
873 /// Releases all resources used by the current instance of <see
874 /// cref="SemaphoreSlim"/>.
875 /// </summary>
876 /// <remarks>
877 /// Unlike most of the members of <see cref="SemaphoreSlim"/>, <see cref="Dispose()"/> is not
878 /// thread-safe and may not be used concurrently with other members of this instance.
879 /// </remarks>
880 public void Dispose()
882 Dispose(true);
883 GC.SuppressFinalize(this);
886 /// <summary>
887 /// When overridden in a derived class, releases the unmanaged resources used by the
888 /// <see cref="System.Threading.ManualResetEventSlim"/>, and optionally releases the managed resources.
889 /// </summary>
890 /// <param name="disposing">true to release both managed and unmanaged resources;
891 /// false to release only unmanaged resources.</param>
892 /// <remarks>
893 /// Unlike most of the members of <see cref="SemaphoreSlim"/>, <see cref="Dispose(bool)"/> is not
894 /// thread-safe and may not be used concurrently with other members of this instance.
895 /// </remarks>
896 protected virtual void Dispose(bool disposing)
898 if (disposing)
900 WaitHandle? wh = m_waitHandle;
901 if (wh != null)
903 wh.Dispose();
904 m_waitHandle = null;
907 m_lockObjAndDisposed.Value = true;
909 m_asyncHead = null;
910 m_asyncTail = null;
914 /// <summary>
915 /// Private helper method to wake up waiters when a cancellationToken gets canceled.
916 /// </summary>
917 private static readonly Action<object?> s_cancellationTokenCanceledEventHandler = new Action<object?>(CancellationTokenCanceledEventHandler);
918 private static void CancellationTokenCanceledEventHandler(object? obj)
920 Debug.Assert(obj is SemaphoreSlim, "Expected a SemaphoreSlim");
921 SemaphoreSlim semaphore = (SemaphoreSlim)obj;
922 lock (semaphore.m_lockObjAndDisposed)
924 Monitor.PulseAll(semaphore.m_lockObjAndDisposed); //wake up all waiters.
928 /// <summary>
929 /// Checks the dispose status by checking the lock object, if it is null means that object
930 /// has been disposed and throw ObjectDisposedException
931 /// </summary>
932 private void CheckDispose()
934 if (m_lockObjAndDisposed.Value)
936 throw new ObjectDisposedException(null, SR.SemaphoreSlim_Disposed);
939 #endregion