@@ -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,51 @@ 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
+ // 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
+
313
367
/// <summary>
314
368
/// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
315
369
/// using a 32-bit unsigned integer to measure the time interval,
@@ -336,15 +390,9 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
336
390
return false ;
337
391
}
338
392
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 ( ) )
342
394
{
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 ;
348
396
}
349
397
350
398
long startTime = 0 ;
@@ -385,6 +433,10 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
385
433
}
386
434
}
387
435
Monitor . Enter ( m_lockObjAndDisposed , ref lockTaken ) ;
436
+ if ( m_threadsTryingFastPathCount > 0 )
437
+ {
438
+ SpinUntilFastPathFinishes ( ) ;
439
+ }
388
440
m_waitCount ++ ;
389
441
390
442
// 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
699
751
if ( cancellationToken . IsCancellationRequested )
700
752
return Task . FromCanceled < bool > ( cancellationToken ) ;
701
753
754
+ if ( TryWaitFastPath ( ) )
755
+ {
756
+ return Task . FromResult ( true ) ;
757
+ }
758
+
702
759
lock ( m_lockObjAndDisposed )
703
760
{
761
+ if ( m_threadsTryingFastPathCount > 0 )
762
+ {
763
+ SpinUntilFastPathFinishes ( ) ;
764
+ }
704
765
// If there are counts available, allow this waiter to succeed.
705
766
if ( m_currentCount > 0 )
706
767
{
@@ -813,6 +874,10 @@ private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long
813
874
// we no longer hold the lock. As such, acquire it.
814
875
lock ( m_lockObjAndDisposed )
815
876
{
877
+ if ( m_threadsTryingFastPathCount > 0 )
878
+ {
879
+ SpinUntilFastPathFinishes ( ) ;
880
+ }
816
881
// Remove the task from the list. If we're successful in doing so,
817
882
// we know that no one else has tried to complete this waiter yet,
818
883
// so we can safely cancel or timeout.
@@ -839,6 +904,38 @@ public int Release()
839
904
return Release ( 1 ) ;
840
905
}
841
906
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
+
842
939
/// <summary>
843
940
/// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
844
941
/// </summary>
@@ -860,19 +957,20 @@ public int Release(int releaseCount)
860
957
nameof ( releaseCount ) , releaseCount , SR . SemaphoreSlim_Release_CountWrong ) ;
861
958
}
862
959
863
- if ( m_waitCount == 0 && m_waitHandle is null && m_asyncHead is null )
960
+ int fastPathResult = TryReleaseFastPath ( releaseCount ) ;
961
+ if ( fastPathResult >= 0 )
864
962
{
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 ;
870
964
}
871
965
872
966
int returnCount ;
873
967
874
968
lock ( m_lockObjAndDisposed )
875
969
{
970
+ if ( m_threadsTryingFastPathCount > 0 )
971
+ {
972
+ SpinUntilFastPathFinishes ( ) ;
973
+ }
876
974
// Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
877
975
int currentCount = m_currentCount ;
878
976
returnCount = currentCount ;
0 commit comments