Uh oh!
There was an error while loading. Please reload this page.
Increase spinning/polling aggressiveness in the thread pool in low-saturation scenarios - #132765
Conversation
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov |
One example of a benchmark sensitive to this is websockets: Too aggressive parking at low saturation results in a catastrophic regression. This PR reverts it all with some extra gain. === net10 === baseline === this PR CC: @BrennanConroy |
EgorBo
commented
Aug 26, 2026
@EgorBot orchard -linux_arm -linux_amd |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR adjusts thread-pool latency heuristics by making worker threads less likely to park immediately after a spurious dispatch when the pool is lightly saturated, while also tuning spin/backoff behavior in the low-level semaphore/backoff helpers to reduce latency impact.
Changes:
- Add a configurable threshold (
SpuriousDispatchNoSpinThreshold) so “no-spin” parking after spurious dispatch only happens when enough other workers remain processing work. - Increase the
LowLevelLifoSemaphoredefault spin limit and ensure additional waiters can be woken to consume remaining signals. - Reduce the maximum per-iteration exponential backoff in
Backoff.Exponentialto cap delay per retry.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs | Adds spurious-dispatch no-spin gating based on remaining processing workers and a new config-controlled threshold. |
| src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs | Increases default spin budget and adds logic to wake additional waiters when signals remain. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Backoff.cs | Lowers exponential backoff cap to reduce worst-case per-iteration spin delay. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs:55
- Local variable
DefaultSpuriousDispatchNoSpinThresholdis not a constant but uses PascalCase, which is inconsistent with the surrounding style (locals are camelCase; PascalCase is used for constants likeDefaultThreadsToKeepAlive). Renaming helps readability and avoids making it look like a constant.
// default to 2/3 of proc count.
// At more than this working threads we start parking threads after a spurious dispatch.
short DefaultSpuriousDispatchNoSpinThreshold = (short)(Environment.ProcessorCount * 2 / 3);
src/libraries/System.Private.CoreLib/src/System/Threading/Backoff.cs:19
- The comment describes the backoff cap in microseconds, but
Thread.SpinWaitis calibrated and hardware-dependent, so the time-based range is more precise than the code can guarantee. Consider describing the cap in terms of SpinWait iterations and optionally mention the time equivalence as approximate.
// To protect against degenerate cases we will cap the per-iteration wait to 2.2–4.4 microseconds.
private const uint MaxExponentialBackoffBits = 7;
JSON benchmark on CB200 (32 cores VM), ==== default/saturated case (512 connections 32 client threads) virtually no change
==== low-saturation scenario (32 connections, 8 client threads) improvement
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
VSadov
commented
Aug 28, 2026
The "AllSubsets" filures appear unrelated to this PR. I see them in other PRs too. |
VSadov
commented
Aug 28, 2026
Thanks! |
VSadov
commented
Aug 28, 2026
Uh oh!
There was an error while loading. Please reload this page.
VSadov
commented
Aug 28, 2026
/backport to release/11.0 |
Started backporting to |
We made some changes in the threadpool to reduce spinning. In particular to reduce fruitless spinning - when a worker thread scanned through the work queue and found no work whatsoever. We would park such thread as a matter of throttling pointless scanning. The change helped in high saturation scenarios as reducing spurious scans reduces waste and lets other threads do useful work.
Unfortunately, in some low-saturation scenarios those spurious scans were load bearing.
In such scenarios some redundancy in terms of spurious scans must be tolerated to provide good latency.
If we park workers too aggressively when we do not have many workers in the first place we will need to rely on waking them up to serve incoming requests. In a bursty case this could be a noticeable regression. In bursty ping-pong kind of scenario, if this happens on both the app and the client ends, the result could be amplified further.
Here we are tuning the heuristic that parks threads after spurious scans to be enabled only when we have more than 2/3 of the proc count workers.
There could be better ways to make use of this signal selectively and we should explore further.
This is a simple enough change that we can do for net11.
The change also increases allowed spin time and lowers the max delay between polls to cap the impact on latency from longer spin, if such happens.
(it makes sense to have per iteration cap lower than the total, we had it the other way)