@@ -40,7 +40,7 @@ public class SemaphoreSlim : IDisposable
40
40
// The number of synchronously waiting threads, it is set to zero in the constructor and increments before blocking the
41
41
// threading and decrements it back after that. It is used as flag for the release call to know if there are
42
42
// waiting threads in the monitor or not.
43
- private int m_waitCount ;
43
+ private volatile int m_waitCount ;
44
44
45
45
/// <summary>
46
46
/// 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
57
57
private volatile ManualResetEvent ? m_waitHandle ;
58
58
59
59
// Head of list representing asynchronous waits on the semaphore.
60
- private TaskNode ? m_asyncHead ;
60
+ private volatile TaskNode ? m_asyncHead ;
61
61
62
62
// Tail of list representing asynchronous waits on the semaphore.
63
63
private TaskNode ? m_asyncTail ;
64
64
65
65
// No maximum constant
66
66
private const int NO_MAXIMUM = int . MaxValue ;
67
67
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
+
68
73
// Task in a linked list of asynchronous waiters
69
74
private sealed class TaskNode : Task < bool >
70
75
{
@@ -106,6 +111,10 @@ public WaitHandle AvailableWaitHandle
106
111
// lock the count to avoid multiple threads initializing the handle if it is null
107
112
lock ( m_lockObjAndDisposed )
108
113
{
114
+ if ( m_threadsTryingFastPathCount > 0 )
115
+ {
116
+ SpinUntilFastPathFinishes ( ) ;
117
+ }
109
118
// The initial state for the wait handle is true if the count is greater than zero
110
119
// false otherwise
111
120
m_waitHandle ??= new ManualResetEvent ( m_currentCount != 0 ) ;
@@ -310,6 +319,49 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
310
319
return WaitCore ( millisecondsTimeout , cancellationToken ) ;
311
320
}
312
321
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
+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null )
339
+ {
340
+ int currentCount = m_currentCount ;
341
+ if ( currentCount > 0 &&
342
+ Interlocked . CompareExchange ( ref m_currentCount , currentCount - 1 , currentCount ) == currentCount )
343
+ {
344
+ result = true ;
345
+ }
346
+ }
347
+ Interlocked . Decrement ( ref m_threadsTryingFastPathCount ) ;
348
+ }
349
+ return result ;
350
+ }
351
+
352
+ /// <summary>
353
+ /// Blocks the current thread until all the threads trying the fast path finish.
354
+ /// </summary>
355
+ [ UnsupportedOSPlatform ( "browser" ) ]
356
+ private void SpinUntilFastPathFinishes ( )
357
+ {
358
+ SpinWait spinner = default ;
359
+ while ( m_threadsTryingFastPathCount > 0 )
360
+ {
361
+ spinner . SpinOnce ( ) ;
362
+ }
363
+ }
364
+
313
365
/// <summary>
314
366
/// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
315
367
/// using a 32-bit unsigned integer to measure the time interval,
@@ -336,15 +388,9 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
336
388
return false ;
337
389
}
338
390
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 )
391
+ if ( TryWaitFastPath ( ) )
342
392
{
343
- int currentCount = m_currentCount ;
344
- if ( currentCount > 0 && Interlocked . CompareExchange ( ref m_currentCount , currentCount - 1 , currentCount ) == currentCount )
345
- {
346
- return true ;
347
- }
393
+ return true ;
348
394
}
349
395
350
396
long startTime = 0 ;
@@ -385,6 +431,10 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
385
431
}
386
432
}
387
433
Monitor . Enter ( m_lockObjAndDisposed , ref lockTaken ) ;
434
+ if ( m_threadsTryingFastPathCount > 0 )
435
+ {
436
+ SpinUntilFastPathFinishes ( ) ;
437
+ }
388
438
m_waitCount ++ ;
389
439
390
440
// If there are any async waiters, for fairness we'll get in line behind
@@ -699,8 +749,17 @@ private Task<bool> WaitAsyncCore(long millisecondsTimeout, CancellationToken can
699
749
if ( cancellationToken . IsCancellationRequested )
700
750
return Task . FromCanceled < bool > ( cancellationToken ) ;
701
751
752
+ if ( TryWaitFastPath ( ) )
753
+ {
754
+ return Task . FromResult ( true ) ;
755
+ }
756
+
702
757
lock ( m_lockObjAndDisposed )
703
758
{
759
+ if ( m_threadsTryingFastPathCount > 0 )
760
+ {
761
+ SpinUntilFastPathFinishes ( ) ;
762
+ }
704
763
// If there are counts available, allow this waiter to succeed.
705
764
if ( m_currentCount > 0 )
706
765
{
@@ -813,6 +872,10 @@ private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long
813
872
// we no longer hold the lock. As such, acquire it.
814
873
lock ( m_lockObjAndDisposed )
815
874
{
875
+ if ( m_threadsTryingFastPathCount > 0 )
876
+ {
877
+ SpinUntilFastPathFinishes ( ) ;
878
+ }
816
879
// Remove the task from the list. If we're successful in doing so,
817
880
// we know that no one else has tried to complete this waiter yet,
818
881
// so we can safely cancel or timeout.
@@ -839,6 +902,36 @@ public int Release()
839
902
return Release ( 1 ) ;
840
903
}
841
904
905
+ /// <summary>
906
+ /// Tries to release the semaphore without taking the lock.
907
+ /// </summary>
908
+ /// <returns>The previous count of the <see cref="SemaphoreSlim"/> if successful; otherwise, -1.</returns>
909
+ private int TryReleaseFastPath ( int releaseCount )
910
+ {
911
+ int result = - 1 ;
912
+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null && m_waitCount == 0 && m_asyncHead is null )
913
+ {
914
+ Interlocked . Increment ( ref m_threadsTryingFastPathCount ) ;
915
+ // It's possible that the lock is taken by now, don't attempt the fast path in that case.
916
+ // However if it's not taken by now, it's safe to attempt the fast path.
917
+ // If the lock is taken after checking this condition, because m_threadsTryingFastPathCount is already greater than 0,
918
+ // the thread holding the lock wouldn't start its operations.
919
+ // The wait handle and async head need to be null and wait count to be zero to take the fast path.
920
+ // Otherwise we have to follow the slow path taking the lock to avoid race conditions.
921
+ if ( ! Monitor . IsEntered ( m_lockObjAndDisposed ) && m_waitHandle is null && m_waitCount == 0 && m_asyncHead is null )
922
+ {
923
+ int currentCount = m_currentCount ;
924
+ if ( m_maxCount - currentCount >= releaseCount &&
925
+ Interlocked . CompareExchange ( ref m_currentCount , currentCount + releaseCount , currentCount ) == currentCount )
926
+ {
927
+ result = currentCount ;
928
+ }
929
+ }
930
+ Interlocked . Decrement ( ref m_threadsTryingFastPathCount ) ;
931
+ }
932
+ return result ;
933
+ }
934
+
842
935
/// <summary>
843
936
/// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
844
937
/// </summary>
@@ -860,19 +953,20 @@ public int Release(int releaseCount)
860
953
nameof ( releaseCount ) , releaseCount , SR . SemaphoreSlim_Release_CountWrong ) ;
861
954
}
862
955
863
- if ( m_waitCount == 0 && m_waitHandle is null && m_asyncHead is null )
956
+ int fastPathResult = TryReleaseFastPath ( releaseCount ) ;
957
+ if ( fastPathResult >= 0 )
864
958
{
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
- }
959
+ return fastPathResult ;
870
960
}
871
961
872
962
int returnCount ;
873
963
874
964
lock ( m_lockObjAndDisposed )
875
965
{
966
+ if ( m_threadsTryingFastPathCount > 0 )
967
+ {
968
+ SpinUntilFastPathFinishes ( ) ;
969
+ }
876
970
// Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
877
971
int currentCount = m_currentCount ;
878
972
returnCount = currentCount ;
0 commit comments