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
91 get { return m_currentCount; }
95 /// Returns a <see cref="System.Threading.WaitHandle"/> that can be used to wait on the semaphore.
97 /// <value>A <see cref="System.Threading.WaitHandle"/> that can be used to wait on the
98 /// semaphore.</value>
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.
105 /// <exception cref="System.ObjectDisposedException">The <see
106 /// cref="SemaphoreSlim"/> has been disposed.</exception>
107 public WaitHandle AvailableWaitHandle
113 // Return it directly if it is not null
114 if (m_waitHandle
!= null)
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
124 m_waitHandle
= new ManualResetEvent(m_currentCount
!= 0);
135 /// Initializes a new instance of the <see cref="SemaphoreSlim"/> class, specifying
136 /// the initial number of requests that can be granted concurrently.
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
)
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.
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
);
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>();
182 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>.
184 /// <exception cref="System.ObjectDisposedException">The current instance has already been
185 /// disposed.</exception>
188 // Call wait with infinite timeout
189 Wait(Timeout
.Infinite
, new CancellationToken());
193 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>, while observing a
194 /// <see cref="System.Threading.CancellationToken"/>.
196 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to
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
);
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.
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.
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());
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"/>.
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.
242 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to
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
);
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.
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());
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"/>.
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
)
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!)
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)
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.
354 Monitor
.Enter(m_lockObjAndDisposed
, ref lockTaken
);
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.
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)
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;
405 else if (oce
!= null)
410 // Exposing wait handle which is lazily initialized if needed
411 if (m_waitHandle
!= null && m_currentCount
== 0)
413 m_waitHandle
.Reset();
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
;
440 /// Local helper function, waits on the monitor until the monitor receives signal or the
441 /// timeout is expired
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
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
475 if (m_countOfWaitersPulsedToWake
!= 0)
477 --m_countOfWaitersPulsedToWake
;
490 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>.
492 /// <returns>A task that will complete when the semaphore has been entered.</returns>
493 public Task
WaitAsync()
495 return WaitAsync(Timeout
.Infinite
, default);
499 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, while observing a
500 /// <see cref="System.Threading.CancellationToken"/>.
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.
506 /// <exception cref="System.ObjectDisposedException">
507 /// The current instance has already been disposed.
509 public Task
WaitAsync(CancellationToken cancellationToken
)
511 return WaitAsync(Timeout
.Infinite
, cancellationToken
);
515 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>,
516 /// using a 32-bit signed integer to measure the time interval.
518 /// <param name="millisecondsTimeout">
519 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
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.
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.
530 public Task
<bool> WaitAsync(int millisecondsTimeout
)
532 return WaitAsync(millisecondsTimeout
, default);
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"/>.
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.
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.
548 /// <exception cref="System.ObjectDisposedException">
549 /// The current instance has already been disposed.
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"/>.
555 public Task
<bool> WaitAsync(TimeSpan timeout
)
557 return WaitAsync(timeout
, default);
561 /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/>, using a <see
562 /// cref="System.TimeSpan"/> to measure the time interval.
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.
568 /// <param name="cancellationToken">
569 /// The <see cref="System.Threading.CancellationToken"/> token to observe.
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.
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"/>.
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
);
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"/>.
598 /// <param name="millisecondsTimeout">
599 /// The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.
601 /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
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.
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.
611 public Task
<bool> WaitAsync(int millisecondsTimeout
, CancellationToken cancellationToken
)
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)
632 if (m_waitHandle
!= null && m_currentCount
== 0) m_waitHandle
.Reset();
635 else if (millisecondsTimeout
== 0)
637 // No counts, if timeout is zero fail fast
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.
645 Debug
.Assert(m_currentCount
== 0, "m_currentCount should never be negative");
646 var asyncWaiter
= CreateAndAddAsyncWaiter();
647 return (millisecondsTimeout
== Timeout
.Infinite
&& !cancellationToken
.CanBeCanceled
) ?
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");
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");
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
;
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
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);
767 /// Exits the <see cref="SemaphoreSlim"/> once.
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>
778 /// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
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
)
793 if (releaseCount
< 1)
795 throw new ArgumentOutOfRangeException(
796 nameof(releaseCount
), releaseCount
, SR
.SemaphoreSlim_Release_CountWrong
);
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)
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)
868 // And return the count
873 /// Releases all resources used by the current instance of <see
874 /// cref="SemaphoreSlim"/>.
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.
880 public void Dispose()
883 GC
.SuppressFinalize(this);
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.
890 /// <param name="disposing">true to release both managed and unmanaged resources;
891 /// false to release only unmanaged resources.</param>
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.
896 protected virtual void Dispose(bool disposing
)
900 WaitHandle
? wh
= m_waitHandle
;
907 m_lockObjAndDisposed
.Value
= true;
915 /// Private helper method to wake up waiters when a cancellationToken gets canceled.
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.
929 /// Checks the dispose status by checking the lock object, if it is null means that object
930 /// has been disposed and throw ObjectDisposedException
932 private void CheckDispose()
934 if (m_lockObjAndDisposed
.Value
)
936 throw new ObjectDisposedException(null, SR
.SemaphoreSlim_Disposed
);