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
12 /// Limits the number of threads that can access a resource or pool of resources concurrently.
16 /// The <see cref="SemaphoreSlim"/> provides a lightweight semaphore class that doesn't
17 /// use Windows kernel semaphores.
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
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
;
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"/>.
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
) { }
83 #region Public properties
86 /// Gets the current count of the <see cref="SemaphoreSlim"/>.
88 /// <value>The current count of the <see cref="SemaphoreSlim"/>.</value>
89 public int CurrentCount
=> m_currentCount
;
92 /// Returns a <see cref="System.Threading.WaitHandle"/> that can be used to wait on the semaphore.
94 /// <value>A <see cref="System.Threading.WaitHandle"/> that can be used to wait on the
95 /// semaphore.</value>
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.
102 /// <exception cref="System.ObjectDisposedException">The <see
103 /// cref="SemaphoreSlim"/> has been disposed.</exception>
104 public WaitHandle AvailableWaitHandle
110 // Return it directly if it is not null
111 if (m_waitHandle
!= null)
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
121 m_waitHandle
= new ManualResetEvent(m_currentCount
!= 0);
132 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
133 /// the initial number of requests that can be granted concurrently.
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
)
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.
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
);
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>();
179 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>.
181 /// <exception cref="System.ObjectDisposedException">The current instance has already been
182 /// disposed.</exception>
185 // Call wait with infinite timeout
186 Wait(Timeout
.Infinite
, new CancellationToken());
190 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, while observing a
191 /// <see cref="System.Threading.CancellationToken"/>.
193 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to
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
);
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.
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.
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());
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"/>.
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.
239 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to
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
);
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.
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());
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"/>.
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
)
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!)
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)
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.
351 Monitor
.Enter(m_lockObjAndDisposed
, ref lockTaken
);
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.
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)
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;
402 else if (oce
!= null)
407 // Exposing wait handle which is lazily initialized if needed
408 if (m_waitHandle
!= null && m_currentCount
== 0)
410 m_waitHandle
.Reset();
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
;
437 /// Local helper function, waits on the monitor until the monitor receives signal or the
438 /// timeout is expired
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
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
472 if (m_countOfWaitersPulsedToWake
!= 0)
474 --m_countOfWaitersPulsedToWake
;
487 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>.
489 /// <returns>A task that will complete when the semaphore has been entered.</returns>
490 public Task
WaitAsync()
492 return WaitAsync(Timeout
.Infinite
, default);
496 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, while observing a
497 /// <see cref="System.Threading.CancellationToken"/>.
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.
503 /// <exception cref="System.ObjectDisposedException">
504 /// The current instance has already been disposed.
506 public Task
WaitAsync(CancellationToken cancellationToken
)
508 return WaitAsync(Timeout
.Infinite
, cancellationToken
);
512 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
513 /// using a 32-bit signed integer to measure the time interval.
515 /// <param name="millisecondsTimeout">
516 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
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.
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.
527 public Task
<bool> WaitAsync(int millisecondsTimeout
)
529 return WaitAsync(millisecondsTimeout
, default);
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"/>.
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.
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.
545 /// <exception cref="System.ObjectDisposedException">
546 /// The current instance has already been disposed.
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"/>.
552 public Task
<bool> WaitAsync(TimeSpan timeout
)
554 return WaitAsync(timeout
, default);
558 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
559 /// cref="System.TimeSpan"/> to measure the time interval.
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.
565 /// <param name="cancellationToken">
566 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
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.
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"/>.
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
);
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"/>.
595 /// <param name="millisecondsTimeout">
596 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
598 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
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.
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.
608 public Task
<bool> WaitAsync(int millisecondsTimeout
, CancellationToken cancellationToken
)
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)
629 if (m_waitHandle
!= null && m_currentCount
== 0) m_waitHandle
.Reset();
632 else if (millisecondsTimeout
== 0)
634 // No counts, if timeout is zero fail fast
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.
642 Debug
.Assert(m_currentCount
== 0, "m_currentCount should never be negative");
643 TaskNode asyncWaiter
= CreateAndAddAsyncWaiter();
644 return (millisecondsTimeout
== Timeout
.Infinite
&& !cancellationToken
.CanBeCanceled
) ?
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");
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");
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
;
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
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);
764 /// Exits the <see cref="SemaphoreSlim"/> once.
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>
775 /// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
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
)
790 if (releaseCount
< 1)
792 throw new ArgumentOutOfRangeException(
793 nameof(releaseCount
), releaseCount
, SR
.SemaphoreSlim_Release_CountWrong
);
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)
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)
865 // And return the count
870 /// Releases all resources used by the current instance of <see
871 /// cref="SemaphoreSlim"/>.
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.
877 public void Dispose()
880 GC
.SuppressFinalize(this);
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.
887 /// <param name="disposing">true to release both managed and unmanaged resources;
888 /// false to release only unmanaged resources.</param>
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.
893 protected virtual void Dispose(bool disposing
)
897 WaitHandle
? wh
= m_waitHandle
;
904 m_lockObjAndDisposed
.Value
= true;
912 /// Private helper method to wake up waiters when a cancellationToken gets canceled.
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.
926 /// Checks the dispose status by checking the lock object, if it is null means that object
927 /// has been disposed and throw ObjectDisposedException
929 private void CheckDispose()
931 if (m_lockObjAndDisposed
.Value
)
933 throw new ObjectDisposedException(null, SR
.SemaphoreSlim_Disposed
);