Skip to content

Commit 5982eaa

Browse files
committed
Fix race conditions
1 parent a37d4cc commit 5982eaa

File tree

1 file changed

+110
-16
lines changed

1 file changed

+110
-16
lines changed

src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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,49 @@ 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+
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+
313365
/// <summary>
314366
/// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>,
315367
/// using a 32-bit unsigned integer to measure the time interval,
@@ -336,15 +388,9 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
336388
return false;
337389
}
338390

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())
342392
{
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;
348394
}
349395

350396
long startTime = 0;
@@ -385,6 +431,10 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
385431
}
386432
}
387433
Monitor.Enter(m_lockObjAndDisposed, ref lockTaken);
434+
if (m_threadsTryingFastPathCount > 0)
435+
{
436+
SpinUntilFastPathFinishes();
437+
}
388438
m_waitCount++;
389439

390440
// 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
699749
if (cancellationToken.IsCancellationRequested)
700750
return Task.FromCanceled<bool>(cancellationToken);
701751

752+
if (TryWaitFastPath())
753+
{
754+
return Task.FromResult(true);
755+
}
756+
702757
lock (m_lockObjAndDisposed)
703758
{
759+
if (m_threadsTryingFastPathCount > 0)
760+
{
761+
SpinUntilFastPathFinishes();
762+
}
704763
// If there are counts available, allow this waiter to succeed.
705764
if (m_currentCount > 0)
706765
{
@@ -813,6 +872,10 @@ private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, long
813872
// we no longer hold the lock. As such, acquire it.
814873
lock (m_lockObjAndDisposed)
815874
{
875+
if (m_threadsTryingFastPathCount > 0)
876+
{
877+
SpinUntilFastPathFinishes();
878+
}
816879
// Remove the task from the list. If we're successful in doing so,
817880
// we know that no one else has tried to complete this waiter yet,
818881
// so we can safely cancel or timeout.
@@ -839,6 +902,36 @@ public int Release()
839902
return Release(1);
840903
}
841904

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+
842935
/// <summary>
843936
/// Exits the <see cref="SemaphoreSlim"/> a specified number of times.
844937
/// </summary>
@@ -860,19 +953,20 @@ public int Release(int releaseCount)
860953
nameof(releaseCount), releaseCount, SR.SemaphoreSlim_Release_CountWrong);
861954
}
862955

863-
if (m_waitCount == 0 && m_waitHandle is null && m_asyncHead is null)
956+
int fastPathResult = TryReleaseFastPath(releaseCount);
957+
if (fastPathResult >= 0)
864958
{
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;
870960
}
871961

872962
int returnCount;
873963

874964
lock (m_lockObjAndDisposed)
875965
{
966+
if (m_threadsTryingFastPathCount > 0)
967+
{
968+
SpinUntilFastPathFinishes();
969+
}
876970
// Read the m_currentCount into a local variable to avoid unnecessary volatile accesses inside the lock.
877971
int currentCount = m_currentCount;
878972
returnCount = currentCount;

0 commit comments

Comments
 (0)