-
Notifications
You must be signed in to change notification settings - Fork 5.4k
threading: lock-free fast path for SemaphoreSlim.WaitAsync #125452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f3740f0
7472e16
b434b5b
fb8aeb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -401,10 +401,14 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo | |
| // that synchronous waiter succeeds so that they have a chance to release. | ||
| Debug.Assert(!waitSuccessful || m_currentCount > 0, | ||
| "If the wait was successful, there should be count available."); | ||
| if (m_currentCount > 0) | ||
| // Use CAS rather than a plain decrement: the lock-free fast path in WaitAsync | ||
| // can decrement m_currentCount concurrently (it holds no lock). | ||
| int currentCount = m_currentCount; | ||
| while (currentCount > 0 && Interlocked.CompareExchange(ref m_currentCount, currentCount - 1, currentCount) != currentCount) | ||
| currentCount = m_currentCount; | ||
| if (currentCount > 0) | ||
| { | ||
| waitSuccessful = true; | ||
| m_currentCount--; | ||
| } | ||
| else if (oce is not null) | ||
| { | ||
|
|
@@ -678,12 +682,41 @@ private Task<bool> WaitAsyncCore(long millisecondsTimeout, CancellationToken can | |
| if (cancellationToken.IsCancellationRequested) | ||
| return Task.FromCanceled<bool>(cancellationToken); | ||
|
|
||
| // Fast path: try a lock-free acquire; falls through to the lock if it fails. | ||
| // Skipped when m_waitHandle is non-null to keep its state consistent under the lock. | ||
| if (m_waitHandle is null) | ||
| { | ||
| int current = m_currentCount; | ||
| // The waiter checks are best-effort: a sync waiter incrementing m_waitCount inside | ||
| // the lock may not be visible yet, but the CAS will fail if the count has changed. | ||
| if (current > 0 | ||
| && Volatile.Read(ref m_asyncHead) is null | ||
| && Volatile.Read(ref m_waitCount) == 0 | ||
| && Interlocked.CompareExchange(ref m_currentCount, current - 1, current) == current) | ||
|
Comment on lines
+692
to
+695
|
||
| { | ||
| // Handle the rare race where AvailableWaitHandle was initialised concurrently. | ||
| if (current == 1 && m_waitHandle is not null) | ||
| { | ||
| lock (m_lockObjAndDisposed) | ||
| { | ||
| if (m_waitHandle is not null && m_currentCount == 0) | ||
| m_waitHandle.Reset(); | ||
| } | ||
| } | ||
| return Task.FromResult(true); | ||
|
Comment on lines
+685
to
+706
|
||
| } | ||
| } | ||
thomhurst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| lock (m_lockObjAndDisposed) | ||
| { | ||
| // If there are counts available, allow this waiter to succeed. | ||
| if (m_currentCount > 0) | ||
| // Use CAS rather than a plain decrement: the lock-free fast path in WaitAsync | ||
| // can decrement m_currentCount concurrently (it holds no lock). | ||
| int current = m_currentCount; | ||
| while (current > 0 && Interlocked.CompareExchange(ref m_currentCount, current - 1, current) != current) | ||
| current = m_currentCount; | ||
| if (current > 0) | ||
| { | ||
| --m_currentCount; | ||
| if (m_waitHandle is not null && m_currentCount == 0) m_waitHandle.Reset(); | ||
| return Task.FromResult(true); | ||
| } | ||
|
|
@@ -899,7 +932,9 @@ public int Release(int releaseCount) | |
| waiterTask.TrySetResult(result: true); | ||
| } | ||
| } | ||
| m_currentCount = currentCount; | ||
| // Use Interlocked.Add (relative delta) rather than an absolute write so that | ||
| // the lock-free CAS fast path in WaitAsync cannot be overwritten. | ||
| Interlocked.Add(ref m_currentCount, currentCount - returnCount); | ||
|
|
||
| // Exposing wait handle if it is not null | ||
| if (m_waitHandle is not null && returnCount == 0 && currentCount > 0) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Debug.Assert that assumes
waitSuccessfulimpliesm_currentCount > 0is no longer valid now thatWaitAsynccan decrementm_currentCountconcurrently without taking the lock. In Debug builds this can fire spuriously (e.g.,WaitUntilCountOrTimeoutobserved a positive count, but the lock-free fast path acquired the last permit before the assertion executes). Consider removing this assert or rewriting it to assert something that remains true under concurrent lock-free decrements (e.g., assert only after the CAS-based acquire attempt succeeds).