@@ -40,7 +40,7 @@ public class SemaphoreSlim : IDisposable
4040 // The number of synchronously waiting threads, it is set to zero in the constructor and increments before blocking the
4141 // threading and decrements it back after that. It is used as flag for the release call to know if there are
4242 // waiting threads in the monitor or not.
43- private int m_waitCount ;
43+ private volatile int m_waitCount ;
4444
4545 /// <summary>
4646 /// This is used to help prevent waking more waiters than necessary. It's not perfect and sometimes more waiters than
@@ -57,14 +57,19 @@ public class SemaphoreSlim : IDisposable
5757 private volatile ManualResetEvent ? m_waitHandle ;
5858
5959 // Head of list representing asynchronous waits on the semaphore.
60- private TaskNode ? m_asyncHead ;
60+ private volatile TaskNode ? m_asyncHead ;
6161
6262 // Tail of list representing asynchronous waits on the semaphore.
6363 private TaskNode ? m_asyncTail ;
6464
6565 // No maximum constant
6666 private const int NO_MAXIMUM = int . MaxValue ;
6767
68+ // Used to track if we are attempting the fast path (without taking the lock).
69+ // Therefore, if another thread takes the lock, shouldn't start its operations until
70+ // all threads finish their attempt to use the fast path.
71+ private volatile int m_threadsTryingFastPathCount ;
72+
6873 // Task in a linked list of asynchronous waiters
6974 private sealed class TaskNode : Task < bool >
7075 {
@@ -106,6 +111,10 @@ public WaitHandle AvailableWaitHandle
106111 // lock the count to avoid multiple threads initializing the handle if it is null
107112 lock ( m_lockObjAndDisposed )
108113 {
114+ if ( m_threadsTryingFastPathCount > 0 )
115+ {
116+ SpinUntilFastPathFinishes ( ) ;
117+ }
109118 // The initial state for the wait handle is true if the count is greater than zero
110119 // false otherwise
111120 m_waitHandle ??= new ManualResetEvent ( m_currentCount != 0 ) ;
@@ -310,6 +319,51 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
310319 return WaitCore ( millisecondsTimeout , cancellationToken ) ;
311320 }
312321
322+ /// <summary>
323+ /// Tries a fast path to wait on the semaphore without taking the lock.
324+ /// </summary>
325+ /// <returns>true if the fast path succeeded; otherwise, false.</returns>
326+ [ UnsupportedOSPlatform ( "browser" ) ]
327+ private bool TryWaitFastPath ( )
328+ {
329+ bool result = false ;
330+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null )
331+ {
332+ Interlocked . Increment ( ref m_threadsTryingFastPathCount ) ;
333+ // It's possible that the lock is taken by now, don't attempt the fast path in that case.
334+ // However if it's not taken by now, it's safe to attempt the fast path.
335+ // If the lock is taken after checking this condition, because m_threadsTryingFastPathCount is already greater than 0,
336+ // the thread holding the lock wouldn't start its operations.
337+ // If the wait handle is not null, we have to follow the slow path taking the lock to avoid race conditions.
338+ // We need to re-evaluate the conditions before proceeding as another thread might have taken the lock, did its work and released it
339+ // before this thread updated m_threadsTryingFastPathCount.
340+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null )
341+ {
342+ int currentCount = m_currentCount ;
343+ if ( currentCount > 0 &&
344+ Interlocked . CompareExchange ( ref m_currentCount , currentCount - 1 , currentCount ) == currentCount )
345+ {
346+ result = true ;
347+ }
348+ }
349+ Interlocked . Decrement ( ref m_threadsTryingFastPathCount ) ;
350+ }
351+ return result ;
352+ }
353+
354+ /// <summary>
355+ /// Blocks the current thread until all the threads trying the fast path finish.
356+ /// </summary>
357+ [ UnsupportedOSPlatform ( "browser" ) ]
358+ private void SpinUntilFastPathFinishes ( )
359+ {
360+ SpinWait spinner = default ;
361+ while ( m_threadsTryingFastPathCount > 0 )
362+ {
363+ spinner . SpinOnce ( ) ;
364+ }
365+ }
366+
313367 /// <summary>
314368 /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
315369 /// using a 32-bit unsigned integer to measure the time interval,
@@ -336,15 +390,9 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
336390 return false ;
337391 }
338392
339- // Perf: If there is no wait handle, we can try entering the semaphore without using the lock.
340- // Otherwise, we would actually need to take the lock to avoid race conditions when calling m_waitHandle.Reset()
341- if ( m_waitHandle is null )
393+ if ( TryWaitFastPath ( ) )
342394 {
343- int currentCount = m_currentCount ;
344- if ( currentCount > 0 && Interlocked . CompareExchange ( ref m_currentCount , currentCount - 1 , currentCount ) == currentCount )
345- {
346- return true ;
347- }
395+ return true ;
348396 }
349397
350398 long startTime = 0 ;
@@ -385,6 +433,10 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
385433 }
386434 }
387435 Monitor . Enter ( m_lockObjAndDisposed , ref lockTaken ) ;
436+ if ( m_threadsTryingFastPathCount > 0 )
437+ {
438+ SpinUntilFastPathFinishes ( ) ;
439+ }
388440 m_waitCount ++ ;
389441
390442 // If there are any async waiters, for fairness we'll get in line behind
@@ -699,8 +751,17 @@ private Task<bool> WaitAsyncCore(long millisecondsTimeout, CancellationToken can
699751 if ( cancellationToken . IsCancellationRequested )
700752 return Task . FromCanceled < bool > ( cancellationToken ) ;
701753
754+ if ( TryWaitFastPath ( ) )
755+ {
756+ return Task . FromResult ( true ) ;
757+ }
758+
702759 lock ( m_lockObjAndDisposed )
703760 {
761+ if ( m_threadsTryingFastPathCount > 0 )
762+ {
763+ SpinUntilFastPathFinishes ( ) ;
764+ }
704765 // If there are counts available, allow this waiter to succeed.
705766 if ( m_currentCount > 0 )
706767 {
@@ -813,6 +874,10 @@ private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long
813874 // we no longer hold the lock. As such, acquire it.
814875 lock ( m_lockObjAndDisposed )
815876 {
877+ if ( m_threadsTryingFastPathCount > 0 )
878+ {
879+ SpinUntilFastPathFinishes ( ) ;
880+ }
816881 // Remove the task from the list. If we're successful in doing so,
817882 // we know that no one else has tried to complete this waiter yet,
818883 // so we can safely cancel or timeout.
@@ -839,6 +904,38 @@ public int Release()
839904 return Release ( 1 ) ;
840905 }
841906
907+ /// <summary>
908+ /// Tries to release the semaphore without taking the lock.
909+ /// </summary>
910+ /// <returns>The previous count of the <see cref="SemaphoreSlim"/> if successful; otherwise, -1.</returns>
911+ private int TryReleaseFastPath ( int releaseCount )
912+ {
913+ int result = - 1 ;
914+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null && m_waitCount == 0 && m_asyncHead is null )
915+ {
916+ Interlocked . Increment ( ref m_threadsTryingFastPathCount ) ;
917+ // It's possible that the lock is taken by now, don't attempt the fast path in that case.
918+ // However if it's not taken by now, it's safe to attempt the fast path.
919+ // If the lock is taken after checking this condition, because m_threadsTryingFastPathCount is already greater than 0,
920+ // the thread holding the lock wouldn't start its operations.
921+ // The wait handle and async head need to be null and wait count to be zero to take the fast path.
922+ // Otherwise we have to follow the slow path taking the lock to avoid race conditions.
923+ // We need to re-evaluate the conditions before proceeding as another thread might have taken the lock, did its work and released it
924+ // before this thread updated m_threadsTryingFastPathCount.
925+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null && m_waitCount == 0 && m_asyncHead is null )
926+ {
927+ int currentCount = m_currentCount ;
928+ if ( m_maxCount - currentCount >= releaseCount &&
929+ Interlocked . CompareExchange ( ref m_currentCount , currentCount + releaseCount , currentCount ) == currentCount )
930+ {
931+ result = currentCount ;
932+ }
933+ }
934+ Interlocked . Decrement ( ref m_threadsTryingFastPathCount ) ;
935+ }
936+ return result ;
937+ }
938+
842939 /// <summary>
843940 /// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
844941 /// </summary>
@@ -860,19 +957,20 @@ public int Release(int releaseCount)
860957 nameof ( releaseCount ) , releaseCount , SR . SemaphoreSlim_Release_CountWrong ) ;
861958 }
862959
863- if ( m_waitCount == 0 && m_waitHandle is null && m_asyncHead is null )
960+ int fastPathResult = TryReleaseFastPath ( releaseCount ) ;
961+ if ( fastPathResult >= 0 )
864962 {
865- int currentCount = m_currentCount ;
866- if ( m_maxCount - currentCount >= releaseCount && Interlocked . CompareExchange ( ref m_currentCount , currentCount + releaseCount , currentCount ) == currentCount )
867- {
868- return currentCount ;
869- }
963+ return fastPathResult ;
870964 }
871965
872966 int returnCount ;
873967
874968 lock ( m_lockObjAndDisposed )
875969 {
970+ if ( m_threadsTryingFastPathCount > 0 )
971+ {
972+ SpinUntilFastPathFinishes ( ) ;
973+ }
876974 // Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
877975 int currentCount = m_currentCount ;
878976 returnCount = currentCount ;
0 commit comments