Skip to content
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

Lock that uses congestion detection for self-tuning #93879

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,27 @@

using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Runtime;
using System.Runtime.CompilerServices;

namespace System.Threading
{
public sealed partial class Lock
{
private const short SpinCountNotInitialized = short.MinValue;

// NOTE: Lock must not have a static (class) constructor, as Lock itself is used to synchronize
// class construction. If Lock has its own class constructor, this can lead to infinite recursion.
// All static data in Lock must be lazy-initialized.
private static int s_staticsInitializationStage;
private static bool s_isSingleProcessor;
private static int s_processorCount;
private static short s_maxSpinCount;
private static short s_minSpinCount;

/// <summary>
/// Initializes a new instance of the <see cref="Lock"/> class.
/// </summary>
public Lock() => _spinCount = SpinCountNotInitialized;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryEnterOneShot(int currentManagedThreadId)
{
Debug.Assert(currentManagedThreadId != 0);

if (State.TryLock(this))
if (this.TryLock())
{
Debug.Assert(_owningThreadId == 0);
Debug.Assert(_recursionCount == 0);
Expand All @@ -55,15 +49,15 @@ internal void Exit(int currentManagedThreadId)

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryEnterSlow(int timeoutMs, int currentManagedThreadId) =>
TryEnterSlow(timeoutMs, new ThreadId((uint)currentManagedThreadId)).IsInitialized;
TryEnterSlow(timeoutMs, new ThreadId((uint)currentManagedThreadId));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool GetIsHeldByCurrentThread(int currentManagedThreadId)
{
Debug.Assert(currentManagedThreadId != 0);

bool isHeld = _owningThreadId == (uint)currentManagedThreadId;
Debug.Assert(!isHeld || new State(this).IsLocked);
Debug.Assert(!isHeld || this.IsLocked);
return isHeld;
}

Expand All @@ -72,14 +66,9 @@ internal uint ExitAll()
Debug.Assert(IsHeldByCurrentThread);

uint recursionCount = _recursionCount;
_owningThreadId = 0;
_recursionCount = 0;

State state = State.Unlock(this);
if (state.HasAnyWaiters)
{
SignalWaiterIfNecessary(state);
}
ReleaseCore();

return recursionCount;
}
Expand All @@ -92,108 +81,83 @@ internal void Reenter(uint previousRecursionCount)
_recursionCount = previousRecursionCount;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TryLockResult LazyInitializeOrEnter()
// Returns false until the static variable is lazy-initialized
internal static bool IsSingleProcessor => s_processorCount == 1;

[MethodImpl(MethodImplOptions.NoInlining)]
internal static void LazyInit()
{
StaticsInitializationStage stage = (StaticsInitializationStage)Volatile.Read(ref s_staticsInitializationStage);
switch (stage)
while (Volatile.Read(ref s_staticsInitializationStage) < (int)StaticsInitializationStage.Usable)
{
case StaticsInitializationStage.Complete:
if (_spinCount == SpinCountNotInitialized)
{
_spinCount = s_maxSpinCount;
}
return TryLockResult.Spin;

case StaticsInitializationStage.Started:
// Spin-wait until initialization is complete or the lock is acquired to prevent class construction cycles
// later during a full wait
bool sleep = true;
while (true)
{
if (sleep)
{
Thread.UninterruptibleSleep0();
}
else
{
Thread.SpinWait(1);
}

stage = (StaticsInitializationStage)Volatile.Read(ref s_staticsInitializationStage);
if (stage == StaticsInitializationStage.Complete)
{
goto case StaticsInitializationStage.Complete;
}
else if (stage == StaticsInitializationStage.NotStarted)
{
goto default;
}

if (State.TryLock(this))
{
return TryLockResult.Locked;
}

sleep = !sleep;
}

default:
Debug.Assert(stage == StaticsInitializationStage.NotStarted);
if (TryInitializeStatics())
{
goto case StaticsInitializationStage.Complete;
}
goto case StaticsInitializationStage.Started;
if (s_staticsInitializationStage == (int)StaticsInitializationStage.NotStarted &&
Interlocked.CompareExchange(
ref s_staticsInitializationStage,
(int)StaticsInitializationStage.Started,
(int)StaticsInitializationStage.NotStarted) == (int)StaticsInitializationStage.NotStarted)
{
ScheduleStaticsInit();
}
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryInitializeStatics()
internal static void ScheduleStaticsInit()
{
// Since Lock is used to synchronize class construction, and some of the statics initialization may involve class
// construction, update the stage first to avoid infinite recursion
switch (
(StaticsInitializationStage)
Interlocked.CompareExchange(
ref s_staticsInitializationStage,
(int)StaticsInitializationStage.Started,
(int)StaticsInitializationStage.NotStarted))
// initialize essentials
// this is safe to do as these do not need to take locks
s_maxSpinCount = DefaultMaxSpinCount << SpinCountScaleShift;
s_minSpinCount = DefaultMinSpinCount << SpinCountScaleShift;

// we can now use the slow path of the lock.
Volatile.Write(ref s_staticsInitializationStage, (int)StaticsInitializationStage.Usable);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock is now functional and did not do anything that could take locks. The rest of initialization is optional and just need to eventually happen.


// other static initialization is optional (but may take locks)
// schedule initialization on finalizer thread to avoid reentrancies.
StaticsInitializer.Schedule();

// trigger an ephemeral GC, in case the app is not allocating anything.
// this will be once per lifetime of the runtime, so it is ok.
GC.Collect(0);
}

private class StaticsInitializer
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Schedule()
{
case StaticsInitializationStage.Started:
return false;
case StaticsInitializationStage.Complete:
return true;
new StaticsInitializer();
}

try
~StaticsInitializer()
{
s_isSingleProcessor = Environment.IsSingleProcessor;
s_maxSpinCount = DetermineMaxSpinCount();
s_minSpinCount = DetermineMinSpinCount();
s_processorCount = RuntimeImports.RhGetProcessCpuCount();
if (s_processorCount > 1)
{
s_minSpinCount = (short)(DetermineMinSpinCount() << SpinCountScaleShift);
s_maxSpinCount = (short)(DetermineMaxSpinCount() << SpinCountScaleShift);
}
else
{
s_minSpinCount = 0;
s_maxSpinCount = 0;
}

// Also initialize some types that are used later to prevent potential class construction cycles
NativeRuntimeEventSource.Log.IsEnabled();
Stopwatch.GetTimestamp();
Volatile.Write(ref s_staticsInitializationStage, (int)StaticsInitializationStage.Complete);
}
catch
{
s_staticsInitializationStage = (int)StaticsInitializationStage.NotStarted;
throw;
}

Volatile.Write(ref s_staticsInitializationStage, (int)StaticsInitializationStage.Complete);
return true;
}

// Returns false until the static variable is lazy-initialized
internal static bool IsSingleProcessor => s_isSingleProcessor;
internal static bool StaticsInitComplete()
{
return Volatile.Read(ref s_staticsInitializationStage) == (int)StaticsInitializationStage.Complete;
}

// Used to transfer the state when inflating thin locks
internal void InitializeLocked(int managedThreadId, uint recursionCount)
{
Debug.Assert(recursionCount == 0 || managedThreadId != 0);

_state = managedThreadId == 0 ? State.InitialStateValue : State.LockedStateValue;
_state = managedThreadId == 0 ? Unlocked : Locked;
_owningThreadId = (uint)managedThreadId;
_recursionCount = recursionCount;
}
Expand All @@ -219,6 +183,7 @@ private enum StaticsInitializationStage
{
NotStarted,
Started,
Usable,
Complete
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ private static unsafe int TryAcquireUncommon(object obj, int currentThreadID, bo
return -1;
}

// rare contention on owned lock,
// rare contention on a lock that we own,
// perhaps hashcode was installed or finalization bits were touched.
// we still own the lock though and may be able to increment, try again
continue;
Expand All @@ -423,12 +423,9 @@ private static unsafe int TryAcquireUncommon(object obj, int currentThreadID, bo
}
}

if (retries != 0)
{
// spin a bit before retrying (1 spinwait is roughly 35 nsec)
// the object is not pinned here
Thread.SpinWaitInternal(i);
}
// spin a bit before retrying (1 spinwait is roughly 35 nsec)
// the object is not pinned here
Thread.SpinWaitInternal(i);
}

// owned by somebody else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ namespace System.Threading
{
public sealed partial class Lock
{
private static readonly short s_maxSpinCount = DetermineMaxSpinCount();
private static readonly short s_minSpinCount = DetermineMinSpinCount();
private static readonly short s_maxSpinCount = (short)(IsSingleProcessor ? 0 :DetermineMaxSpinCount() << SpinCountScaleShift);
private static readonly short s_minSpinCount = (short)(IsSingleProcessor ? 0 :DetermineMinSpinCount() << SpinCountScaleShift);

/// <summary>
/// Initializes a new instance of the <see cref="Lock"/> class.
/// </summary>
public Lock() => _spinCount = s_maxSpinCount;

private static TryLockResult LazyInitializeOrEnter() => TryLockResult.Spin;
private static void LazyInit() { }
private static bool StaticsInitComplete() => true;
private static bool IsSingleProcessor => Environment.IsSingleProcessor;

internal partial struct ThreadId
Expand Down
Loading
Loading