-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Fix Lock spin-waiting on single-proc machines #101513
Changes from all commits
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 |
---|---|---|
|
@@ -37,7 +37,15 @@ public sealed partial class Lock | |
|
||
private uint _state; // see State for layout | ||
private uint _recursionCount; | ||
|
||
// This field serves a few purposes currently: | ||
// - When positive, it indicates the number of spin-wait iterations that most threads would do upon contention | ||
// - When zero, it indicates that spin-waiting is to be attempted by a thread to test if it is successful | ||
// - When negative, it serves as a rough counter for contentions that would increment it towards zero | ||
// | ||
// See references to this field and "AdaptiveSpin" in TryEnterSlow for more information. | ||
private short _spinCount; | ||
|
||
private ushort _waiterStartTimeMs; | ||
private AutoResetEvent? _waitEvent; | ||
|
||
|
@@ -297,7 +305,7 @@ private void ExitImpl() | |
} | ||
} | ||
|
||
private static bool IsAdaptiveSpinEnabled(short minSpinCount) => minSpinCount <= 0; | ||
private static bool IsAdaptiveSpinEnabled(short minSpinCountForAdaptiveSpin) => minSpinCountForAdaptiveSpin <= 0; | ||
|
||
[MethodImpl(MethodImplOptions.NoInlining)] | ||
private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | ||
|
@@ -339,20 +347,19 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
goto Locked; | ||
} | ||
|
||
bool isSingleProcessor = IsSingleProcessor; | ||
short maxSpinCount = s_maxSpinCount; | ||
if (maxSpinCount == 0) | ||
{ | ||
goto Wait; | ||
} | ||
|
||
short minSpinCount = s_minSpinCount; | ||
short minSpinCountForAdaptiveSpin = s_minSpinCountForAdaptiveSpin; | ||
short spinCount = _spinCount; | ||
if (spinCount < 0) | ||
{ | ||
// When negative, the spin count serves as a counter for contentions such that a spin-wait can be attempted | ||
// periodically to see if it would be beneficial. Increment the spin count and skip spin-waiting. | ||
Debug.Assert(IsAdaptiveSpinEnabled(minSpinCount)); | ||
Debug.Assert(IsAdaptiveSpinEnabled(minSpinCountForAdaptiveSpin)); | ||
_spinCount = (short)(spinCount + 1); | ||
goto Wait; | ||
} | ||
|
@@ -377,7 +384,7 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
|
||
for (short spinIndex = 0; ;) | ||
{ | ||
LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor); | ||
LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor: false); | ||
|
||
if (++spinIndex >= spinCount) | ||
{ | ||
|
@@ -394,7 +401,7 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
|
||
if (tryLockResult == TryLockResult.Locked) | ||
{ | ||
if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCount)) | ||
if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCountForAdaptiveSpin)) | ||
{ | ||
// Since the first spinner does a full-length spin-wait, and to keep upward and downward changes to the | ||
// spin count more balanced, only the first spinner adjusts the spin count | ||
|
@@ -415,7 +422,7 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
|
||
// Unregister the spinner and try to acquire the lock | ||
tryLockResult = State.TryLockAfterSpinLoop(this); | ||
if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCount)) | ||
if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCountForAdaptiveSpin)) | ||
{ | ||
// Since the first spinner does a full-length spin-wait, and to keep upward and downward changes to the | ||
// spin count more balanced, only the first spinner adjusts the spin count | ||
|
@@ -433,7 +440,7 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
// number of contentions, the first spinner will attempt a spin-wait again to see if it is effective. | ||
Debug.Assert(tryLockResult == TryLockResult.Wait); | ||
spinCount = _spinCount; | ||
_spinCount = spinCount > 0 ? (short)(spinCount - 1) : minSpinCount; | ||
_spinCount = spinCount > 0 ? (short)(spinCount - 1) : minSpinCountForAdaptiveSpin; | ||
} | ||
} | ||
|
||
|
@@ -504,7 +511,7 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) | |
break; | ||
} | ||
|
||
LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor); | ||
LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor: false); | ||
} | ||
|
||
if (acquiredLock) | ||
|
@@ -648,14 +655,25 @@ internal nint LockIdForEvents | |
|
||
internal ulong OwningThreadId => _owningThreadId; | ||
|
||
private static short DetermineMaxSpinCount() => | ||
AppContextConfigHelper.GetInt16Config( | ||
"System.Threading.Lock.SpinCount", | ||
"DOTNET_Lock_SpinCount", | ||
DefaultMaxSpinCount, | ||
allowNegative: false); | ||
private static short DetermineMaxSpinCount() | ||
{ | ||
if (IsSingleProcessor) | ||
{ | ||
return 0; | ||
} | ||
|
||
return | ||
AppContextConfigHelper.GetInt16Config( | ||
"System.Threading.Lock.SpinCount", | ||
"DOTNET_Lock_SpinCount", | ||
DefaultMaxSpinCount, | ||
allowNegative: false); | ||
} | ||
|
||
private static short DetermineMinSpinCount() | ||
// When the returned value is zero or negative, indicates the lowest value that the _spinCount field will have when | ||
// adaptive spin chooses to pause spin-waiting, see the comment on the _spinCount field for more information. When the | ||
// returned value is positive, adaptive spin is disabled. | ||
private static short DetermineMinSpinCountForAdaptiveSpin() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is a Min and Max spin count - the actual spincount is obviously computed to be somewhere in the range. Not sure the name change makes it more or less clear. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The min spin count with adaptive spin is just used as a limit for how low the _spinCount field can go, it can be negative, in which case it's just used as a counter for contentions while skipping spin-waiting until it reaches a value where it tries spin-waiting again. Maybe there's a better name, I can add a comment as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is ok either way.
|
||
{ | ||
// The config var can be set to -1 to disable adaptive spin | ||
short adaptiveSpinPeriod = | ||
|
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.
Is this the actual fix? - to force "no spin" in singleproc case, even if user specified something max spin count.
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.
Yea that's the actual fix