diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs index 9fd823ba175708..ab3e7ad42504c1 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @@ -183,10 +183,12 @@ public static void PulseAll(object obj) ObjPulseAll(obj); } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) /// /// Gets the number of times there was contention upon trying to take a 's lock so far. /// - public static long LockContentionCount => GetLockContentionCount(); + public static long LockContentionCount => GetLockContentionCount() + Lock.ContentionCount; +#pragma warning restore CA2252 [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ObjectNative_GetMonitorLockContentionCount")] private static partial long GetLockContentionCount(); diff --git a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifier.cs b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifier.cs index 237895a8c0711b..73c81ddf45797c 100644 --- a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifier.cs +++ b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifier.cs @@ -79,7 +79,7 @@ protected ConcurrentUnifier() public V GetOrAdd(K key) { Debug.Assert(key != null); - Debug.Assert(!_lock.IsAcquired, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); + Debug.Assert(!_lock.IsHeldByCurrentThread, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); int hashCode = key.GetHashCode(); V value; @@ -89,7 +89,7 @@ public V GetOrAdd(K key) V checkedValue; bool checkedFound; // In debug builds, always exercise a locked TryGet (this is a good way to detect deadlock/reentrancy through Equals/GetHashCode()). - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { _container.VerifyUnifierConsistency(); int h = key.GetHashCode(); @@ -110,7 +110,7 @@ public V GetOrAdd(K key) value = this.Factory(key); - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { V heyIWasHereFirst; if (_container.TryGetValue(key, hashCode, out heyIWasHereFirst)) @@ -171,7 +171,7 @@ public bool TryGetValue(K key, int hashCode, out V value) public void Add(K key, int hashCode, V value) { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); int bucket = ComputeBucket(hashCode, _buckets.Length); @@ -194,14 +194,14 @@ public bool HasCapacity { get { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); return _nextFreeEntry != _entries.Length; } } public void Resize() { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); int newSize = HashHelpers.GetPrime(_buckets.Length * 2); #if DEBUG @@ -257,7 +257,7 @@ public void VerifyUnifierConsistency() if (_nextFreeEntry >= 5000 && (0 != (_nextFreeEntry % 100))) return; - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); Debug.Assert(_nextFreeEntry >= 0 && _nextFreeEntry <= _entries.Length); int numEntriesEncountered = 0; for (int bucket = 0; bucket < _buckets.Length; bucket++) diff --git a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierW.cs b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierW.cs index 049ce1ee078ab2..321419e4aa5c0c 100644 --- a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierW.cs +++ b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierW.cs @@ -89,7 +89,7 @@ protected ConcurrentUnifierW() public V GetOrAdd(K key) { Debug.Assert(key != null); - Debug.Assert(!_lock.IsAcquired, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); + Debug.Assert(!_lock.IsHeldByCurrentThread, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); int hashCode = key.GetHashCode(); V? value; @@ -99,7 +99,7 @@ public V GetOrAdd(K key) V? checkedValue; bool checkedFound; // In debug builds, always exercise a locked TryGet (this is a good way to detect deadlock/reentrancy through Equals/GetHashCode()). - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { _container.VerifyUnifierConsistency(); int h = key.GetHashCode(); @@ -137,7 +137,7 @@ public V GetOrAdd(K key) return null; } - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { V? heyIWasHereFirst; if (_container.TryGetValue(key, hashCode, out heyIWasHereFirst)) @@ -201,7 +201,7 @@ public bool TryGetValue(K key, int hashCode, out V? value) public void Add(K key, int hashCode, V value) { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); int bucket = ComputeBucket(hashCode, _buckets.Length); @@ -251,14 +251,14 @@ public bool HasCapacity { get { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); return _nextFreeEntry != _entries.Length; } } public void Resize() { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); // Before we actually grow the size of the table, figure out how much we can recover just by dropping entries with // expired weak references. @@ -341,7 +341,7 @@ public void VerifyUnifierConsistency() if (_nextFreeEntry >= 5000 || (0 != (_nextFreeEntry % 100))) return; - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); Debug.Assert(_nextFreeEntry >= 0 && _nextFreeEntry <= _entries.Length); int numEntriesEncountered = 0; for (int bucket = 0; bucket < _buckets.Length; bucket++) diff --git a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierWKeyed.cs b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierWKeyed.cs index 48708eac12a982..22c0fb5f680527 100644 --- a/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierWKeyed.cs +++ b/src/coreclr/nativeaot/Common/src/System/Collections/Concurrent/ConcurrentUnifierWKeyed.cs @@ -102,7 +102,7 @@ protected ConcurrentUnifierWKeyed() public V GetOrAdd(K key) { Debug.Assert(key != null); - Debug.Assert(!_lock.IsAcquired, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); + Debug.Assert(!_lock.IsHeldByCurrentThread, "GetOrAdd called while lock already acquired. A possible cause of this is an Equals or GetHashCode method that causes reentrancy in the table."); int hashCode = key.GetHashCode(); V value; @@ -112,7 +112,7 @@ public V GetOrAdd(K key) V checkedValue; bool checkedFound; // In debug builds, always exercise a locked TryGet (this is a good way to detect deadlock/reentrancy through Equals/GetHashCode()). - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { _container.VerifyUnifierConsistency(); int h = key.GetHashCode(); @@ -154,7 +154,7 @@ public V GetOrAdd(K key) // it needs to produce the key quickly and in a deadlock-free manner once we're inside the lock. value.PrepareKey(); - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { V heyIWasHereFirst; if (_container.TryGetValue(key, hashCode, out heyIWasHereFirst)) @@ -220,7 +220,7 @@ public bool TryGetValue(K key, int hashCode, out V value) public void Add(int hashCode, V value) { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); int bucket = ComputeBucket(hashCode, _buckets.Length); int newEntryIdx = _nextFreeEntry; @@ -241,14 +241,14 @@ public bool HasCapacity { get { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); return _nextFreeEntry != _entries.Length; } } public void Resize() { - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); // Before we actually grow the size of the table, figure out how much we can recover just by dropping entries with // expired weak references. @@ -330,7 +330,7 @@ public void VerifyUnifierConsistency() if (_nextFreeEntry >= 5000 && (0 != (_nextFreeEntry % 100))) return; - Debug.Assert(_owner._lock.IsAcquired); + Debug.Assert(_owner._lock.IsHeldByCurrentThread); Debug.Assert(_nextFreeEntry >= 0 && _nextFreeEntry <= _entries.Length); int numEntriesEncountered = 0; for (int bucket = 0; bucket < _buckets.Length; bucket++) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml b/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml index 2347f0973c539d..50058746b67b33 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml @@ -929,14 +929,6 @@ CP0001 T:System.Threading.Condition - - CP0001 - T:System.Threading.Lock - - - CP0001 - T:System.Threading.LockHolder - CP0002 M:System.ModuleHandle.#ctor(System.Reflection.Module) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/SynchronizedMethodHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/SynchronizedMethodHelpers.cs index 4f4db6916b511c..56701f033500b1 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/SynchronizedMethodHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/SynchronizedMethodHelpers.cs @@ -14,7 +14,8 @@ internal static class SynchronizedMethodHelpers private static void MonitorEnter(object obj, ref bool lockTaken) { // Inlined Monitor.Enter with a few tweaks - int resultOrIndex = ObjectHeader.Acquire(obj); + int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; + int resultOrIndex = ObjectHeader.Acquire(obj, currentThreadID); if (resultOrIndex < 0) { lockTaken = true; @@ -25,7 +26,7 @@ private static void MonitorEnter(object obj, ref bool lockTaken) ObjectHeader.GetLockObject(obj) : SyncTable.GetLockObject(resultOrIndex); - Monitor.TryAcquireSlow(lck, obj, Timeout.Infinite); + lck.TryEnterSlow(Timeout.Infinite, currentThreadID, obj); lockTaken = true; } private static void MonitorExit(object obj, ref bool lockTaken) @@ -42,7 +43,8 @@ private static unsafe void MonitorEnterStatic(MethodTable* pMT, ref bool lockTak { // Inlined Monitor.Enter with a few tweaks object obj = GetStaticLockObject(pMT); - int resultOrIndex = ObjectHeader.Acquire(obj); + int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; + int resultOrIndex = ObjectHeader.Acquire(obj, currentThreadID); if (resultOrIndex < 0) { lockTaken = true; @@ -53,7 +55,7 @@ private static unsafe void MonitorEnterStatic(MethodTable* pMT, ref bool lockTak ObjectHeader.GetLockObject(obj) : SyncTable.GetLockObject(resultOrIndex); - Monitor.TryAcquireSlow(lck, obj, Timeout.Infinite); + lck.TryEnterSlow(Timeout.Infinite, currentThreadID, obj); lockTaken = true; } private static unsafe void MonitorExitStatic(MethodTable* pMT, ref bool lockTaken) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index a007a3ed7fbba5..12ae4d8a6b0c45 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -3,6 +3,8 @@ true $(NoWarn);AD0001 + + true @@ -231,10 +233,9 @@ - + - @@ -305,9 +306,6 @@ Interop\Unix\System.Native\Interop.Exit.cs - - Interop\Unix\System.Native\Interop.Threading.cs - diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/ClassConstructorRunner.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/ClassConstructorRunner.cs index 8093c0f71ff147..7e8293e1a1653f 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/ClassConstructorRunner.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/ClassConstructorRunner.cs @@ -111,7 +111,7 @@ public static unsafe void EnsureClassConstructorRun(StaticClassConstructionConte cctors[cctorIndex].HoldingThread = ManagedThreadIdNone; NoisyLog("Releasing cctor lock, context={0}, thread={1}", pContext, currentManagedThreadId); - cctorLock.Release(); + cctorLock.Exit(); } } else @@ -142,10 +142,10 @@ private static unsafe bool DeadlockAwareAcquire(CctorHandle cctor, StaticClassCo int cctorIndex = cctor.Index; Cctor[] cctors = cctor.Array; Lock lck = cctors[cctorIndex].Lock; - if (lck.IsAcquired) + if (lck.IsHeldByCurrentThread) return false; // Thread recursively triggered the same cctor. - if (lck.TryAcquire(waitIntervalInMS)) + if (lck.TryEnter(waitIntervalInMS)) return true; // We couldn't acquire the lock. See if this .cctor is involved in a cross-thread deadlock. If so, break @@ -164,7 +164,7 @@ private static unsafe bool DeadlockAwareAcquire(CctorHandle cctor, StaticClassCo // deadlock themselves, then that's a bug in user code. for (;;) { - using (LockHolder.Hold(s_cctorGlobalLock)) + using (s_cctorGlobalLock.EnterScope()) { // Ask the guy who holds the cctor lock we're trying to acquire who he's waiting for. Keep // walking down that chain until we either discover a cycle or reach a non-blocking state. Note @@ -233,7 +233,7 @@ private static unsafe bool DeadlockAwareAcquire(CctorHandle cctor, StaticClassCo waitIntervalInMS *= 2; // We didn't find a cycle yet, try to take the lock again. - if (lck.TryAcquire(waitIntervalInMS)) + if (lck.TryEnter(waitIntervalInMS)) return true; } // infinite loop } @@ -283,7 +283,7 @@ public static CctorHandle GetCctor(StaticClassConstructionContext* pContext) } #endif // TARGET_WASM - using (LockHolder.Hold(s_cctorGlobalLock)) + using (s_cctorGlobalLock.EnterScope()) { Cctor[]? resultArray = null; int resultIndex = -1; @@ -355,14 +355,14 @@ public static int Count { get { - Debug.Assert(s_cctorGlobalLock.IsAcquired); + Debug.Assert(s_cctorGlobalLock.IsHeldByCurrentThread); return s_count; } } public static void Release(CctorHandle cctor) { - using (LockHolder.Hold(s_cctorGlobalLock)) + using (s_cctorGlobalLock.EnterScope()) { Cctor[] cctors = cctor.Array; int cctorIndex = cctor.Index; @@ -419,7 +419,7 @@ public static int MarkThreadAsBlocked(int managedThreadId, CctorHandle blockedOn #else const int Grow = 10; #endif - using (LockHolder.Hold(s_cctorGlobalLock)) + using (s_cctorGlobalLock.EnterScope()) { s_blockingRecords ??= new BlockingRecord[Grow]; int found; @@ -450,14 +450,14 @@ public static int MarkThreadAsBlocked(int managedThreadId, CctorHandle blockedOn public static void UnmarkThreadAsBlocked(int blockRecordIndex) { // This method must never throw - s_cctorGlobalLock.Acquire(); + s_cctorGlobalLock.Enter(); s_blockingRecords[blockRecordIndex].BlockedOn = new CctorHandle(null, 0); - s_cctorGlobalLock.Release(); + s_cctorGlobalLock.Exit(); } public static CctorHandle GetCctorThatThreadIsBlockedOn(int managedThreadId) { - Debug.Assert(s_cctorGlobalLock.IsAcquired); + Debug.Assert(s_cctorGlobalLock.IsHeldByCurrentThread); for (int i = 0; i < s_nextBlockingRecordIndex; i++) { if (s_blockingRecords[i].ManagedThreadId == managedThreadId) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.NativeAot.cs index e776860562ca49..0fc6330a1c88e4 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.NativeAot.cs @@ -951,7 +951,7 @@ private unsafe bool TryGetOrCreateObjectForComInstanceInternal( if (!flags.HasFlag(CreateObjectFlags.UniqueInstance)) { - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { if (_rcwCache.TryGetValue(identity, out GCHandle handle)) { @@ -1047,7 +1047,7 @@ private unsafe bool TryGetOrCreateObjectForComInstanceInternal( return true; } - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { object? cachedWrapper = null; if (_rcwCache.TryGetValue(identity, out var existingHandle)) @@ -1092,7 +1092,7 @@ private unsafe bool TryGetOrCreateObjectForComInstanceInternal( private void RemoveRCWFromCache(IntPtr comPointer, GCHandle expectedValue) { - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { // TryGetOrCreateObjectForComInstanceInternal may have put a new entry into the cache // in the time between the GC cleared the contents of the GC handle but before the diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs index a567debf03708e..c8fdbb60384348 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs @@ -56,7 +56,7 @@ private unsafe void AssertIsNotInList(Waiter waiter) private unsafe void AddWaiter(Waiter waiter) { - Debug.Assert(_lock.IsAcquired); + Debug.Assert(_lock.IsHeldByCurrentThread); AssertIsNotInList(waiter); waiter.prev = _waitersTail; @@ -70,7 +70,7 @@ private unsafe void AddWaiter(Waiter waiter) private unsafe void RemoveWaiter(Waiter waiter) { - Debug.Assert(_lock.IsAcquired); + Debug.Assert(_lock.IsHeldByCurrentThread); AssertIsInList(waiter); if (waiter.next != null) @@ -101,13 +101,13 @@ public unsafe bool Wait(int millisecondsTimeout) { ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1); - if (!_lock.IsAcquired) + if (!_lock.IsHeldByCurrentThread) throw new SynchronizationLockException(); Waiter waiter = GetWaiterForCurrentThread(); AddWaiter(waiter); - uint recursionCount = _lock.ReleaseAll(); + uint recursionCount = _lock.ExitAll(); bool success = false; try { @@ -115,8 +115,8 @@ public unsafe bool Wait(int millisecondsTimeout) } finally { - _lock.Reacquire(recursionCount); - Debug.Assert(_lock.IsAcquired); + _lock.Reenter(recursionCount); + Debug.Assert(_lock.IsHeldByCurrentThread); if (!waiter.signalled) { @@ -140,7 +140,7 @@ public unsafe bool Wait(int millisecondsTimeout) public unsafe void SignalAll() { - if (!_lock.IsAcquired) + if (!_lock.IsHeldByCurrentThread) throw new SynchronizationLockException(); while (_waitersHead != null) @@ -149,7 +149,7 @@ public unsafe void SignalAll() public unsafe void SignalOne() { - if (!_lock.IsAcquired) + if (!_lock.IsHeldByCurrentThread) throw new SynchronizationLockException(); Waiter? waiter = _waitersHead; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.NativeAot.cs new file mode 100644 index 00000000000000..690014f91691fa --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.NativeAot.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Tracing; +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 short s_maxSpinCount; + private static short s_minSpinCount; + + /// + /// Initializes a new instance of the class. + /// + public Lock() => _spinCount = SpinCountNotInitialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryEnterOneShot(int currentManagedThreadId) + { + Debug.Assert(currentManagedThreadId != 0); + + if (State.TryLock(this)) + { + Debug.Assert(_owningThreadId == 0); + Debug.Assert(_recursionCount == 0); + _owningThreadId = (uint)currentManagedThreadId; + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Exit(int currentManagedThreadId) + { + Debug.Assert(currentManagedThreadId != 0); + + if (_owningThreadId != (uint)currentManagedThreadId) + { + ThrowHelper.ThrowSynchronizationLockException_LockExit(); + } + + ExitImpl(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) => + TryEnterSlow(timeoutMs, currentThreadId, this); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryEnterSlow(int timeoutMs, int currentManagedThreadId, object associatedObject) => + TryEnterSlow(timeoutMs, new ThreadId((uint)currentManagedThreadId), associatedObject).IsInitialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool GetIsHeldByCurrentThread(int currentManagedThreadId) + { + Debug.Assert(currentManagedThreadId != 0); + + bool isHeld = _owningThreadId == (uint)currentManagedThreadId; + Debug.Assert(!isHeld || new State(this).IsLocked); + return isHeld; + } + + internal uint ExitAll() + { + Debug.Assert(IsHeldByCurrentThread); + + uint recursionCount = _recursionCount; + _owningThreadId = 0; + _recursionCount = 0; + + State state = State.Unlock(this); + if (state.HasAnyWaiters) + { + SignalWaiterIfNecessary(state); + } + + return recursionCount; + } + + internal void Reenter(uint previousRecursionCount) + { + Debug.Assert(!IsHeldByCurrentThread); + + Enter(); + _recursionCount = previousRecursionCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TryLockResult LazyInitializeOrEnter() + { + StaticsInitializationStage stage = (StaticsInitializationStage)Volatile.Read(ref s_staticsInitializationStage); + switch (stage) + { + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryInitializeStatics() + { + // 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)) + { + case StaticsInitializationStage.Started: + return false; + case StaticsInitializationStage.Complete: + return true; + } + + try + { + s_isSingleProcessor = Environment.IsSingleProcessor; + s_maxSpinCount = DetermineMaxSpinCount(); + s_minSpinCount = DetermineMinSpinCount(); + + // Also initialize some types that are used later to prevent potential class construction cycles + NativeRuntimeEventSource.Log.IsEnabled(); + } + 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; + + // 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; + _owningThreadId = (uint)managedThreadId; + _recursionCount = recursionCount; + } + + internal struct ThreadId + { + private uint _id; + + public ThreadId(uint id) => _id = id; + public uint Id => _id; + public bool IsInitialized => _id != 0; + public static ThreadId Current_NoInitialize => new ThreadId((uint)ManagedThreadId.CurrentManagedThreadIdUnchecked); + + public void InitializeForCurrentThread() + { + Debug.Assert(!IsInitialized); + _id = (uint)ManagedThreadId.Current; + Debug.Assert(IsInitialized); + } + } + + private enum StaticsInitializationStage + { + NotStarted, + Started, + Complete + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.cs deleted file mode 100644 index 30bc946ad42546..00000000000000 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.cs +++ /dev/null @@ -1,544 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.IO; -using System.Runtime; -using System.Runtime.CompilerServices; - -namespace System.Threading -{ - public sealed class Lock : IDisposable - { - // - // This lock is a hybrid spinning/blocking lock with dynamically adjusted spinning. - // On a multiprocessor machine an acquiring thread will try to acquire multiple times - // before going to sleep. The amount of spinning is dynamically adjusted based on past - // history of the lock and will stay in the following range. - // - // We use doubling-up delays with a cap while spinning (1,2,4,8,16,32,64,64,64,64, ...) - // Thus 20 iterations is about 1000 speenwaits (20-50 ns each) - // Context switch costs may vary and typically in 2-20 usec range - // Even if we are the only thread trying to acquire the lock at 20-50 usec the cost of being - // blocked+awaken may not be more than 2x of what we have already spent, so that is the max CPU time - // that we will allow to burn while spinning. - // - // This may not be always optimal, but should be close enough. - // I.E. in a system consisting of exactly 2 threads, unlimited spinning may work better, but we - // will not optimize specifically for that. - private const ushort MaxSpinLimit = 20; - private const ushort MinSpinLimit = 3; - private const ushort SpinningNotInitialized = MaxSpinLimit + 1; - private const ushort SpinningDisabled = 0; - - // - // We will use exponential backoff in rare cases when we need to change state atomically and cannot - // make progress due to concurrent state changes by other threads. - // While we cannot know the ideal amount of wait needed before making a successfull attempt, - // the exponential backoff will generally be not more than 2X worse than the perfect guess and - // will do a lot less attempts than an simple retry. On multiprocessor machine fruitless attempts - // will cause unnecessary sharing of the contended state which may make modifying the state more expensive. - // To protect against degenerate cases we will cap the per-iteration wait to 1024 spinwaits. - // - private const uint MaxExponentialBackoffBits = 10; - - // - // This lock is unfair and permits acquiring a contended lock by a nonwaiter in the presence of waiters. - // It is possible for one thread to keep holding the lock long enough that waiters go to sleep and - // then release and reacquire fast enough that waiters have no chance to get the lock. - // In extreme cases one thread could keep retaking the lock starving everybody else. - // If we see woken waiters not able to take the lock for too long we will ask nonwaiters to wait. - // - private const uint WaiterWatchdogTicks = 100; - - // - // 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. - // - internal static int s_processorCount; - - // - // m_state layout: - // - // bit 0: True if the lock is held, false otherwise. - // - // bit 1: True if we've set the event to wake a waiting thread. The waiter resets this to false when it - // wakes up. This avoids the overhead of setting the event multiple times. - // - // bit 2: True if nonwaiters must not get ahead of waiters when acquiring a contended lock. - // - // everything else: A count of the number of threads waiting on the event. - // - private const int Uncontended = 0; - private const int Locked = 1; - private const int WaiterWoken = 2; - private const int YieldToWaiters = 4; - private const int WaiterCountIncrement = 8; - - // state of the lock - private AutoResetEvent? _lazyEvent; - private int _owningThreadId; - private uint _recursionCount; - private int _state; - private ushort _spinLimit = SpinningNotInitialized; - private short _wakeWatchDog; - - // used to transfer the state when inflating thin locks - internal void InitializeLocked(int threadId, int recursionCount) - { - Debug.Assert(recursionCount == 0 || threadId != 0); - - _state = threadId == 0 ? Uncontended : Locked; - _owningThreadId = threadId; - _recursionCount = (uint)recursionCount; - } - - private AutoResetEvent Event - { - get - { - if (_lazyEvent == null) - Interlocked.CompareExchange(ref _lazyEvent, new AutoResetEvent(false), null); - - return _lazyEvent; - } - } - - public void Dispose() - { - _lazyEvent?.Dispose(); - } - - private static int CurrentThreadId => Environment.CurrentManagedThreadId; - - [MethodImpl(MethodImplOptions.NoInlining)] - public void Acquire() - { - int currentThreadId = CurrentThreadId; - if (TryAcquireOneShot(currentThreadId)) - return; - - // - // Fall back to the slow path for contention - // - bool success = TryAcquireSlow(currentThreadId, Timeout.Infinite); - Debug.Assert(success); - } - - public bool TryAcquire(TimeSpan timeout) - { - return TryAcquire(WaitHandle.ToTimeoutMilliseconds(timeout)); - } - - public bool TryAcquire(int millisecondsTimeout) - { - ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1); - - int currentThreadId = CurrentThreadId; - if (TryAcquireOneShot(currentThreadId)) - return true; - - // - // Fall back to the slow path for contention - // - return TryAcquireSlow(currentThreadId, millisecondsTimeout, trackContentions: false); - } - - internal bool TryAcquireNoSpin() - { - // - // Make one quick attempt to acquire an uncontended lock - // - int currentThreadId = CurrentThreadId; - if (TryAcquireOneShot(currentThreadId)) - return true; - - // - // If we already own the lock, just increment the recursion count. - // - if (_owningThreadId == currentThreadId) - { - checked { _recursionCount++; } - return true; - } - - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool TryAcquireOneShot(int currentThreadId) - { - int origState = _state; - int expectedState = origState & ~(YieldToWaiters | Locked); - int newState = origState | Locked; - if (Interlocked.CompareExchange(ref _state, newState, expectedState) == expectedState) - { - Debug.Assert(_owningThreadId == 0); - Debug.Assert(_recursionCount == 0); - _owningThreadId = currentThreadId; - return true; - } - - return false; - } - - private static unsafe void ExponentialBackoff(uint iteration) - { - if (iteration > 0) - { - // no need for much randomness here, we will just hash the stack address + iteration. - uint rand = ((uint)&iteration + iteration) * 2654435769u; - // set the highmost bit to ensure minimum number of spins is exponentialy increasing - // that is in case some stack location results in a sequence of very low spin counts - // it basically gurantees that we spin at least 1, 2, 4, 8, 16, times, and so on - rand |= (1u << 31); - uint spins = rand >> (byte)(32 - Math.Min(iteration, MaxExponentialBackoffBits)); - Thread.SpinWaitInternal((int)spins); - } - } - - internal bool TryAcquireSlow(int currentThreadId, int millisecondsTimeout, bool trackContentions = false) - { - // - // If we already own the lock, just increment the recursion count. - // - if (_owningThreadId == currentThreadId) - { - checked { _recursionCount++; } - return true; - } - - // - // We've already made one lock attempt at this point, so bail early if the timeout is zero. - // - if (millisecondsTimeout == 0) - return false; - - // since we have just made an attempt to accuire and failed, do a small pause - Thread.SpinWaitInternal(1); - - if (_spinLimit == SpinningNotInitialized) - { - // Use RhGetProcessCpuCount directly to avoid Environment.ProcessorCount->ClassConstructorRunner->Lock->Environment.ProcessorCount cycle - if (s_processorCount == 0) - s_processorCount = RuntimeImports.RhGetProcessCpuCount(); - - _spinLimit = (s_processorCount > 1) ? MinSpinLimit : SpinningDisabled; - } - - bool hasWaited = false; - // we will retry after waking up - while (true) - { - uint iteration = 0; - - // We will count when we failed to change the state of the lock and increase pauses - // so that bursts of activity are better tolerated. This should not happen often. - uint collisions = 0; - - // We will track the changes of ownership while we are trying to acquire the lock. - int oldOwner = _owningThreadId; - uint ownerChanged = 0; - - uint localSpinLimit = _spinLimit; - // inner loop where we try acquiring the lock or registering as a waiter - while (true) - { - // - // Try to grab the lock. We may take the lock here even if there are existing waiters. This creates the possibility - // of starvation of waiters, but it also prevents lock convoys and preempted waiters from destroying perf. - // However, if we do not see _wakeWatchDog cleared for long enough, we go into YieldToWaiters mode to ensure some - // waiter progress. - // - int oldState = _state; - bool canAcquire = ((oldState & Locked) == 0) && - (hasWaited || ((oldState & YieldToWaiters) == 0)); - - if (canAcquire) - { - int newState = oldState | Locked; - if (hasWaited) - newState = (newState - WaiterCountIncrement) & ~(WaiterWoken | YieldToWaiters); - - if (Interlocked.CompareExchange(ref _state, newState, oldState) == oldState) - { - // GOT THE LOCK!! - if (hasWaited) - _wakeWatchDog = 0; - - // now we can estimate how busy the lock is and adjust spinning accordingly - ushort spinLimit = _spinLimit; - if (ownerChanged != 0) - { - // The lock has changed ownership while we were trying to acquire it. - // It is a signal that we might want to spin less next time. - // Pursuing a lock that is being "stolen" by other threads is inefficient - // due to cache misses and unnecessary sharing of state that keeps invalidating. - if (spinLimit > MinSpinLimit) - { - _spinLimit = (ushort)(spinLimit - 1); - } - } - else if (spinLimit < MaxSpinLimit && iteration > spinLimit / 2) - { - // we used more than 50% of allowed iterations, but the lock does not look very contested, - // we can allow a bit more spinning. - _spinLimit = (ushort)(spinLimit + 1); - } - - Debug.Assert((_state | Locked) != 0); - Debug.Assert(_owningThreadId == 0); - Debug.Assert(_recursionCount == 0); - _owningThreadId = currentThreadId; - return true; - } - } - - if (iteration++ < localSpinLimit) - { - int newOwner = _owningThreadId; - if (newOwner != 0 && newOwner != oldOwner) - { - ownerChanged++; - oldOwner = newOwner; - } - - if (canAcquire) - { - collisions++; - } - - // We failed to acquire the lock and want to retry after a pause. - // Ideally we will retry right when the lock becomes free, but we cannot know when that will happen. - // We will use a pause that doubles up on every iteration. It will not be more than 2x worse - // than the ideal guess, while minimizing the number of retries. - // We will allow pauses up to 64~128 spinwaits, or more if there are collisions. - ExponentialBackoff(Math.Min(iteration, 6) + collisions); - continue; - } - else if (!canAcquire) - { - // - // We reached our spin limit, and need to wait. Increment the waiter count. - // Note that we do not do any overflow checking on this increment. In order to overflow, - // we'd need to have about 1 billion waiting threads, which is inconceivable anytime in the - // forseeable future. - // - int newState = oldState + WaiterCountIncrement; - if (hasWaited) - newState = (newState - WaiterCountIncrement) & ~WaiterWoken; - - if (Interlocked.CompareExchange(ref _state, newState, oldState) == oldState) - break; - - collisions++; - } - - ExponentialBackoff(collisions); - } - - // - // Now we wait. - // - - if (trackContentions) - { - Monitor.IncrementLockContentionCount(); - } - - TimeoutTracker timeoutTracker = TimeoutTracker.Start(millisecondsTimeout); - Debug.Assert(_state >= WaiterCountIncrement); - bool waitSucceeded = Event.WaitOne(millisecondsTimeout); - Debug.Assert(_state >= WaiterCountIncrement); - - if (!waitSucceeded) - break; - - // we did not time out and will try acquiring the lock - hasWaited = true; - millisecondsTimeout = timeoutTracker.Remaining; - } - - // We timed out. We're not going to wait again. - { - uint iteration = 0; - while (true) - { - int oldState = _state; - Debug.Assert(oldState >= WaiterCountIncrement); - - int newState = oldState - WaiterCountIncrement; - - // We could not have consumed a wake, or the wait would've succeeded. - // If we are the last waiter though, we will clear WaiterWoken and YieldToWaiters - // just so that lock would not look like contended. - if (newState < WaiterCountIncrement) - newState = newState & ~WaiterWoken & ~YieldToWaiters; - - if (Interlocked.CompareExchange(ref _state, newState, oldState) == oldState) - return false; - - ExponentialBackoff(iteration++); - } - } - } - - public bool IsAcquired - { - get - { - // - // Compare the current owning thread ID with the current thread ID. We need - // to read the current thread's ID before we read m_owningThreadId. Otherwise, - // the following might happen: - // - // 1) We read m_owningThreadId, and get, say 42, which belongs to another thread. - // 2) Thread 42 releases the lock, and exits. - // 3) We call ManagedThreadId.Current. If this is the first time it's been called - // on this thread, we'll go get a new ID. We may reuse thread 42's ID, since - // that thread is dead. - // 4) Now we're thread 42, and it looks like we own the lock, even though we don't. - // - // However, as long as we get this thread's ID first, we know it won't be reused, - // because while we're doing this check the current thread is definitely still - // alive. - // - int currentThreadId = CurrentThreadId; - return IsAcquiredByThread(currentThreadId); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool IsAcquiredByThread(int currentThreadId) - { - bool acquired = (currentThreadId == _owningThreadId); - Debug.Assert(!acquired || (_state & Locked) != 0); - return acquired; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public void Release() - { - ReleaseByThread(CurrentThreadId); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void ReleaseByThread(int threadId) - { - if (threadId != _owningThreadId) - throw new SynchronizationLockException(); - - if (_recursionCount == 0) - { - ReleaseCore(); - return; - } - - _recursionCount--; - } - - internal uint ReleaseAll() - { - Debug.Assert(IsAcquired); - - uint recursionCount = _recursionCount; - _recursionCount = 0; - - ReleaseCore(); - - return recursionCount; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReleaseCore() - { - Debug.Assert(_recursionCount == 0); - _owningThreadId = 0; - int origState = Interlocked.Decrement(ref _state); - if (origState < WaiterCountIncrement || (origState & WaiterWoken) != 0) - { - return; - } - - // - // We have waiters; take the slow path. - // - AwakeWaiterIfNeeded(); - } - - private void AwakeWaiterIfNeeded() - { - uint iteration = 0; - while (true) - { - int oldState = _state; - if (oldState >= WaiterCountIncrement && (oldState & WaiterWoken) == 0) - { - // there are waiters, and nobody has woken one. - int newState = oldState | WaiterWoken; - - short lastWakeTicks = _wakeWatchDog; - if (lastWakeTicks != 0 && (short)Environment.TickCount - lastWakeTicks > WaiterWatchdogTicks) - { - newState |= YieldToWaiters; - } - - if (Interlocked.CompareExchange(ref _state, newState, oldState) == oldState) - { - if (lastWakeTicks == 0) - { - // nonzero timestamp of the last wake - _wakeWatchDog = (short)(Environment.TickCount | 1); - } - - Event.Set(); - return; - } - } - else - { - // no need to wake a waiter. - return; - } - - ExponentialBackoff(iteration++); - } - } - - internal void Reacquire(uint previousRecursionCount) - { - Acquire(); - Debug.Assert(_recursionCount == 0); - _recursionCount = previousRecursionCount; - } - - internal struct TimeoutTracker - { - private int _start; - private int _timeout; - - public static TimeoutTracker Start(int timeout) - { - TimeoutTracker tracker = new TimeoutTracker(); - tracker._timeout = timeout; - if (timeout != Timeout.Infinite) - tracker._start = Environment.TickCount; - return tracker; - } - - public int Remaining - { - get - { - if (_timeout == Timeout.Infinite) - return Timeout.Infinite; - int elapsed = Environment.TickCount - _start; - if (elapsed > _timeout) - return 0; - return _timeout - elapsed; - } - } - } - } -} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/LockHolder.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/LockHolder.cs deleted file mode 100644 index 784a5d0fe3555e..00000000000000 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/LockHolder.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -namespace System.Threading -{ - public struct LockHolder : IDisposable - { - private Lock _lock; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static LockHolder Hold(Lock l) - { - LockHolder h; - l.Acquire(); - h._lock = l; - return h; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() - { - _lock.Release(); - } - } -} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs index ce2e58a975abf0..89a8505663d307 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs @@ -41,7 +41,8 @@ private static Condition GetCondition(object obj) [MethodImpl(MethodImplOptions.NoInlining)] public static void Enter(object obj) { - int resultOrIndex = ObjectHeader.Acquire(obj); + int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; + int resultOrIndex = ObjectHeader.Acquire(obj, currentThreadID); if (resultOrIndex < 0) return; @@ -49,7 +50,7 @@ public static void Enter(object obj) ObjectHeader.GetLockObject(obj) : SyncTable.GetLockObject(resultOrIndex); - TryAcquireSlow(lck, obj, Timeout.Infinite); + lck.TryEnterSlow(Timeout.Infinite, currentThreadID, obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -66,7 +67,8 @@ public static void Enter(object obj, ref bool lockTaken) [MethodImpl(MethodImplOptions.NoInlining)] public static bool TryEnter(object obj) { - int resultOrIndex = ObjectHeader.TryAcquire(obj); + int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; + int resultOrIndex = ObjectHeader.TryAcquire(obj, currentThreadID); if (resultOrIndex < 0) return true; @@ -74,7 +76,13 @@ public static bool TryEnter(object obj) return false; Lock lck = SyncTable.GetLockObject(resultOrIndex); - return lck.TryAcquire(0); + + // The one-shot fast path is not covered by the slow path below for a zero timeout when the thread ID is + // initialized, so cover it here in case it wasn't already done + if (currentThreadID != 0 && lck.TryEnterOneShot(currentThreadID)) + return true; + + return lck.TryEnterSlow(0, currentThreadID, obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -92,7 +100,8 @@ public static bool TryEnter(object obj, int millisecondsTimeout) { ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1); - int resultOrIndex = ObjectHeader.TryAcquire(obj); + int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; + int resultOrIndex = ObjectHeader.TryAcquire(obj, currentThreadID); if (resultOrIndex < 0) return true; @@ -100,10 +109,12 @@ public static bool TryEnter(object obj, int millisecondsTimeout) ObjectHeader.GetLockObject(obj) : SyncTable.GetLockObject(resultOrIndex); - if (millisecondsTimeout == 0) - return lck.TryAcquireNoSpin(); + // The one-shot fast path is not covered by the slow path below for a zero timeout when the thread ID is + // initialized, so cover it here in case it wasn't already done + if (millisecondsTimeout == 0 && currentThreadID != 0 && lck.TryEnterOneShot(currentThreadID)) + return true; - return TryAcquireSlow(lck, obj, millisecondsTimeout); + return lck.TryEnterSlow(millisecondsTimeout, currentThreadID, obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -159,18 +170,6 @@ public static void PulseAll(object obj) #endregion - #region Slow path for Entry/TryEnter methods. - - internal static bool TryAcquireSlow(Lock lck, object obj, int millisecondsTimeout) - { - using (new DebugBlockingScope(obj, DebugBlockingItemType.MonitorCriticalSection, millisecondsTimeout, out _)) - { - return lck.TryAcquireSlow(Environment.CurrentManagedThreadId, millisecondsTimeout, trackContentions: true); - } - } - - #endregion - #region Debugger support // The debugger binds to the fields below by name. Do not change any names or types without @@ -185,14 +184,14 @@ internal static bool TryAcquireSlow(Lock lck, object obj, int millisecondsTimeou // Different ways a thread can be blocked that the debugger will expose. // Do not change or add members without updating the debugger code. - private enum DebugBlockingItemType + internal enum DebugBlockingItemType { MonitorCriticalSection = 0, MonitorEvent = 1 } // Represents an item a thread is blocked on. This structure is allocated on the stack and accessed by the debugger. - private struct DebugBlockingItem + internal struct DebugBlockingItem { // The object the thread is waiting on public object _object; @@ -207,7 +206,7 @@ private struct DebugBlockingItem public IntPtr _next; } - private unsafe struct DebugBlockingScope : IDisposable + internal unsafe struct DebugBlockingScope : IDisposable { public DebugBlockingScope(object obj, DebugBlockingItemType blockingType, int timeout, out DebugBlockingItem blockingItem) { @@ -229,28 +228,10 @@ public void Dispose() #region Metrics - private static readonly ThreadInt64PersistentCounter s_lockContentionCounter = new ThreadInt64PersistentCounter(); - - [ThreadStatic] - private static object t_ContentionCountObject; - - [MethodImpl(MethodImplOptions.NoInlining)] - private static object CreateThreadLocalContentionCountObject() - { - Debug.Assert(t_ContentionCountObject == null); - - object threadLocalContentionCountObject = s_lockContentionCounter.CreateThreadLocalCountObject(); - t_ContentionCountObject = threadLocalContentionCountObject; - return threadLocalContentionCountObject; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void IncrementLockContentionCount() => ThreadInt64PersistentCounter.Increment(t_ContentionCountObject ?? CreateThreadLocalContentionCountObject()); - /// /// Gets the number of times there was contention upon trying to take a 's lock so far. /// - public static long LockContentionCount => s_lockContentionCounter.Count; + public static long LockContentionCount => Lock.ContentionCount; #endregion } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/ObjectHeader.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/ObjectHeader.cs index 327586771fe074..d411f997ee11a4 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/ObjectHeader.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/ObjectHeader.cs @@ -217,7 +217,7 @@ public static unsafe void SetSyncEntryIndex(int* pHeader, int syncIndex) { // Holding this lock implies there is at most one thread setting the sync entry index at // any given time. We also require that the sync entry index has not been already set. - Debug.Assert(SyncTable.s_lock.IsAcquired); + Debug.Assert(SyncTable.s_lock.IsHeldByCurrentThread); int oldBits, newBits; do @@ -239,7 +239,7 @@ public static unsafe void SetSyncEntryIndex(int* pHeader, int syncIndex) SyncTable.MoveThinLockToNewEntry( syncIndex, oldBits & SBLK_MASK_LOCK_THREADID, - (oldBits & SBLK_MASK_LOCK_RECLEVEL) >> SBLK_RECLEVEL_SHIFT); + (uint)((oldBits & SBLK_MASK_LOCK_RECLEVEL) >> SBLK_RECLEVEL_SHIFT)); } // Store the sync entry index @@ -284,24 +284,22 @@ public static unsafe void SetSyncEntryIndex(int* pHeader, int syncIndex) // 0 - failed // syncIndex - retry with the Lock [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Acquire(object obj) + public static unsafe int Acquire(object obj, int currentThreadID) { - return TryAcquire(obj, oneShot: false); + return TryAcquire(obj, currentThreadID, oneShot: false); } // -1 - success // 0 - failed // syncIndex - retry with the Lock [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int TryAcquire(object obj, bool oneShot = true) + public static unsafe int TryAcquire(object obj, int currentThreadID, bool oneShot = true) { ArgumentNullException.ThrowIfNull(obj); Debug.Assert(!(obj is Lock), "Do not use Monitor.Enter or TryEnter on a Lock instance; use Lock methods directly instead."); - int currentThreadID = ManagedThreadId.CurrentManagedThreadIdUnchecked; - // if thread ID is uninitialized or too big, we do "uncommon" part. if ((uint)(currentThreadID - 1) <= (uint)SBLK_MASK_LOCK_THREADID) { @@ -323,7 +321,7 @@ public static unsafe int TryAcquire(object obj, bool oneShot = true) } else if (GetSyncEntryIndex(oldBits, out int syncIndex)) { - if (SyncTable.GetLockObject(syncIndex).TryAcquireOneShot(currentThreadID)) + if (SyncTable.GetLockObject(syncIndex).TryEnterOneShot(currentThreadID)) { return -1; } @@ -334,23 +332,25 @@ public static unsafe int TryAcquire(object obj, bool oneShot = true) } } - return TryAcquireUncommon(obj, oneShot); + return TryAcquireUncommon(obj, currentThreadID, oneShot); } // handling uncommon cases here - recursive lock, contention, retries // -1 - success // 0 - failed // syncIndex - retry with the Lock - private static unsafe int TryAcquireUncommon(object obj, bool oneShot) + private static unsafe int TryAcquireUncommon(object obj, int currentThreadID, bool oneShot) { + if (currentThreadID == 0) + currentThreadID = Environment.CurrentManagedThreadId; + // does thread ID fit? - int currentThreadID = Environment.CurrentManagedThreadId; if (currentThreadID > SBLK_MASK_LOCK_THREADID) return GetSyncIndex(obj); - // Lock.s_processorCount is lazy-initialized at fist contended acquire - // untill then it is 0 and we assume we have multicore machine - int retries = oneShot || Lock.s_processorCount == 1 ? 0 : 16; + // Lock.IsSingleProcessor gets a value that is lazy-initialized at the first contended acquire. + // Until then it is false and we assume we have multicore machine. + int retries = oneShot || Lock.IsSingleProcessor ? 0 : 16; // retry when the lock is owned by somebody else. // this loop will spinwait between iterations. @@ -422,9 +422,12 @@ private static unsafe int TryAcquireUncommon(object obj, bool oneShot) } } - // spin a bit before retrying (1 spinwait is roughly 35 nsec) - // the object is not pinned here - Thread.SpinWaitInternal(i); + if (retries != 0) + { + // spin a bit before retrying (1 spinwait is roughly 35 nsec) + // the object is not pinned here + Thread.SpinWaitInternal(i); + } } // owned by somebody else @@ -481,7 +484,7 @@ public static unsafe void Release(object obj) } } - fatLock.ReleaseByThread(currentThreadID); + fatLock.Exit(currentThreadID); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -510,7 +513,7 @@ public static unsafe bool IsAcquired(object obj) if (GetSyncEntryIndex(oldBits, out int syncIndex)) { - return SyncTable.GetLockObject(syncIndex).IsAcquiredByThread(currentThreadID); + return SyncTable.GetLockObject(syncIndex).GetIsHeldByCurrentThread(currentThreadID); } // someone else owns or noone. diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/SyncTable.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/SyncTable.cs index de07c986834ec1..02d7b4167ca6b2 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/SyncTable.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/SyncTable.cs @@ -106,7 +106,7 @@ public static unsafe int AssignEntry(object obj, int* pHeader) try { - using (LockHolder.Hold(s_lock)) + using (s_lock.EnterScope()) { // After acquiring the lock check whether another thread already assigned the sync entry if (ObjectHeader.GetSyncEntryIndex(*pHeader, out int syncIndex)) @@ -172,7 +172,7 @@ public static unsafe int AssignEntry(object obj, int* pHeader) /// private static void Grow() { - Debug.Assert(s_lock.IsAcquired); + Debug.Assert(s_lock.IsHeldByCurrentThread); int oldSize = s_entries.Length; int newSize = CalculateNewSize(oldSize); @@ -242,7 +242,7 @@ public static int SetHashCode(int syncIndex, int hashCode) // Acquire the lock to ensure we are updating the latest version of s_entries. This // lock may be avoided if we store the hash code and Monitor synchronization data in // the same object accessed by a reference. - using (LockHolder.Hold(s_lock)) + using (s_lock.EnterScope()) { int currentHash = s_entries[syncIndex].HashCode; if (currentHash != 0) @@ -260,7 +260,7 @@ public static int SetHashCode(int syncIndex, int hashCode) /// public static void MoveHashCodeToNewEntry(int syncIndex, int hashCode) { - Debug.Assert(s_lock.IsAcquired); + Debug.Assert(s_lock.IsHeldByCurrentThread); Debug.Assert((0 < syncIndex) && (syncIndex < s_unusedEntryIndex)); s_entries[syncIndex].HashCode = hashCode; } @@ -269,9 +269,9 @@ public static void MoveHashCodeToNewEntry(int syncIndex, int hashCode) /// Initializes the Lock assuming the caller holds s_lock. Use for not yet /// published entries only. /// - public static void MoveThinLockToNewEntry(int syncIndex, int threadId, int recursionLevel) + public static void MoveThinLockToNewEntry(int syncIndex, int threadId, uint recursionLevel) { - Debug.Assert(s_lock.IsAcquired); + Debug.Assert(s_lock.IsHeldByCurrentThread); Debug.Assert((0 < syncIndex) && (syncIndex < s_unusedEntryIndex)); s_entries[syncIndex].Lock.InitializeLocked(threadId, recursionLevel); @@ -305,7 +305,7 @@ public DeadEntryCollector() Lock? lockToDispose = default; DependentHandle dependentHandleToDispose = default; - using (LockHolder.Hold(s_lock)) + using (s_lock.EnterScope()) { ref Entry entry = ref s_entries[_index]; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 467e13cfd60338..7d8c2f2a2f290f 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -251,7 +251,7 @@ private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) if (this != CurrentThread) { - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { if (HasStarted()) throw new ThreadStateException(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index cda114276746c8..ef35ed5358fafc 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -230,7 +230,7 @@ public ThreadPriority Priority } // Prevent race condition with starting this thread - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { if (HasStarted() && !SetPriorityLive(value)) { @@ -358,7 +358,7 @@ public static void SpinWait(int iterations) private void StartCore() { - using (LockHolder.Hold(_lock)) + using (_lock.EnterScope()) { if (!GetThreadStateBit(ThreadState.Unstarted)) { diff --git a/src/coreclr/nativeaot/System.Private.DisabledReflection/src/System.Private.DisabledReflection.csproj b/src/coreclr/nativeaot/System.Private.DisabledReflection/src/System.Private.DisabledReflection.csproj index 163f809979cc42..25af7c63691ee8 100644 --- a/src/coreclr/nativeaot/System.Private.DisabledReflection/src/System.Private.DisabledReflection.csproj +++ b/src/coreclr/nativeaot/System.Private.DisabledReflection/src/System.Private.DisabledReflection.csproj @@ -2,6 +2,8 @@ false + + true diff --git a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj index 5527d5920f6018..2610014e0d3c14 100644 --- a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj +++ b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj @@ -8,6 +8,8 @@ $(CompilerCommonPath)\Internal\NativeFormat false + + true diff --git a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj index cc44031e9ac552..620a94d91c40ab 100644 --- a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj +++ b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj @@ -2,6 +2,8 @@ false + + true diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericMethodsLookup.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericMethodsLookup.cs index 9fe72daf5045db..228da851da728f 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericMethodsLookup.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericMethodsLookup.cs @@ -274,7 +274,7 @@ public bool TryGetGenericVirtualMethodPointer(InstantiatedMethod method, out Int if (!TryLookupGenericMethodDictionary(new MethodDescBasedGenericMethodLookup(method), out dictionaryPointer)) { - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { // Now that we hold the lock, we may find that existing types can now find // their associated RuntimeTypeHandle. Flush the type builder states as a way @@ -297,7 +297,7 @@ private bool TryGetDynamicGenericMethodDictionary(GenericMethodLookupData lookup { result = IntPtr.Zero; - using (LockHolder.Hold(_dynamicGenericsLock)) + using (_dynamicGenericsLock.EnterScope()) { GenericMethodEntry entry; if (!_dynamicGenericMethods.TryGetValue(lookupData, out entry)) @@ -349,7 +349,7 @@ private bool TryGetDynamicGenericMethodComponents(IntPtr methodDictionary, out R methodNameAndSignature = null; genericMethodArgumentHandles = null; - using (LockHolder.Hold(_dynamicGenericsLock)) + using (_dynamicGenericsLock.EnterScope()) { GenericMethodEntry entry; if (!_dynamicGenericMethodComponents.TryGetValue(methodDictionary, out entry)) diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericTypesLookup.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericTypesLookup.cs index 2db0a24b5586d9..257803bd9026ef 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericTypesLookup.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericTypesLookup.cs @@ -217,7 +217,7 @@ public bool TryLookupConstructedGenericTypeForComponents(RuntimeTypeHandle gener public bool TryLookupConstructedLazyDictionaryForContext(IntPtr context, IntPtr signature, out IntPtr dictionary) { - Debug.Assert(_typeLoaderLock.IsAcquired); + Debug.Assert(_typeLoaderLock.IsHeldByCurrentThread); return _lazyGenericDictionaries.TryGetValue(new LazyDictionaryContext { _context = context, _signature = signature }, out dictionary); } @@ -226,7 +226,7 @@ private unsafe bool TryGetDynamicGenericTypeForComponents(GenericTypeLookupData { runtimeTypeHandle = default(RuntimeTypeHandle); - using (LockHolder.Hold(_dynamicGenericsLock)) + using (_dynamicGenericsLock.EnterScope()) { GenericTypeEntry entry; if (!_dynamicGenericTypes.TryGetValue(lookupData, out entry)) diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericsRegistration.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericsRegistration.cs index 8c9922d1a29c73..cebd5d917896fe 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericsRegistration.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.ConstructedGenericsRegistration.cs @@ -28,7 +28,7 @@ internal struct DynamicGenericsRegistrationData internal void RegisterDynamicGenericTypesAndMethods(DynamicGenericsRegistrationData registrationData) { - using (LockHolder.Hold(_dynamicGenericsLock)) + using (_dynamicGenericsLock.EnterScope()) { int registeredTypesCount = 0; int registeredMethodsCount = 0; @@ -130,7 +130,7 @@ internal void RegisterDynamicGenericTypesAndMethods(DynamicGenericsRegistrationD public void RegisterConstructedLazyDictionaryForContext(IntPtr context, IntPtr signature, IntPtr dictionary) { - Debug.Assert(_typeLoaderLock.IsAcquired); + Debug.Assert(_typeLoaderLock.IsHeldByCurrentThread); _lazyGenericDictionaries.Add(new LazyDictionaryContext { _context = context, _signature = signature }, dictionary); } } diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.LdTokenResultLookup.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.LdTokenResultLookup.cs index 8cc5e5844218fa..ad8c68b171e5c0 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.LdTokenResultLookup.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.LdTokenResultLookup.cs @@ -57,7 +57,7 @@ private static unsafe string GetStringFromMemoryInNativeFormat(IntPtr pointerToD /// public IntPtr GetNativeFormatStringForString(string str) { - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { IntPtr result; if (_nativeFormatStrings.TryGetValue(str, out result)) diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.StaticsLookup.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.StaticsLookup.cs index 4a28836cb33ce9..28cd9bf3276380 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.StaticsLookup.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.StaticsLookup.cs @@ -145,7 +145,7 @@ public IntPtr TryGetThreadStaticFieldData(RuntimeTypeHandle runtimeTypeHandle) public IntPtr GetThreadStaticGCDescForDynamicType(TypeManagerHandle typeManagerHandle, uint index) { - using (LockHolder.Hold(_threadStaticsLock)) + using (_threadStaticsLock.EnterScope()) { return _dynamicGenericsThreadStaticDescs[typeManagerHandle.GetIntPtrUNSAFE()][index]; } @@ -168,7 +168,7 @@ public void RegisterDynamicThreadStaticsInfo(RuntimeTypeHandle runtimeTypeHandle IntPtr typeManager = runtimeTypeHandle.GetTypeManager().GetIntPtrUNSAFE(); - _threadStaticsLock.Acquire(); + _threadStaticsLock.Enter(); try { if (!_dynamicGenericsThreadStaticDescs.TryGetValue(typeManager, out LowLevelDictionary gcDescs)) @@ -188,7 +188,7 @@ public void RegisterDynamicThreadStaticsInfo(RuntimeTypeHandle runtimeTypeHandle } } - _threadStaticsLock.Release(); + _threadStaticsLock.Exit(); } } #endregion diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.cs index ab037c2b41e39c..1bd3cbd3672fac 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeLoaderEnvironment.cs @@ -144,13 +144,13 @@ internal static void Initialize() public void VerifyTypeLoaderLockHeld() { - if (!_typeLoaderLock.IsAcquired) + if (!_typeLoaderLock.IsHeldByCurrentThread) Environment.FailFast("TypeLoaderLock not held"); } public void RunUnderTypeLoaderLock(Action action) { - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { action(); } @@ -160,7 +160,7 @@ public IntPtr GenericLookupFromContextAndSignature(IntPtr context, IntPtr signat { IntPtr result; - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { try { @@ -191,7 +191,7 @@ private bool EnsureTypeHandleForType(TypeDesc type) { if (type.RuntimeTypeHandle.IsNull()) { - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { // Now that we hold the lock, we may find that existing types can now find // their associated RuntimeTypeHandle. Flush the type builder states as a way @@ -340,7 +340,7 @@ public bool TryGetConstructedGenericTypeForComponents(RuntimeTypeHandle genericT if (TryLookupConstructedGenericTypeForComponents(genericTypeDefinitionHandle, genericTypeArgumentHandles, out runtimeTypeHandle)) return true; - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { return TypeBuilder.TryBuildGenericType(genericTypeDefinitionHandle, genericTypeArgumentHandles, out runtimeTypeHandle); } @@ -351,7 +351,7 @@ public bool TryGetFunctionPointerTypeForComponents(RuntimeTypeHandle returnTypeH if (TryLookupFunctionPointerTypeForComponents(returnTypeHandle, parameterHandles, isUnmanaged, out runtimeTypeHandle)) return true; - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { return TypeBuilder.TryBuildFunctionPointerType(returnTypeHandle, parameterHandles, isUnmanaged, out runtimeTypeHandle); } @@ -390,7 +390,7 @@ public bool TryGetArrayTypeForElementType(RuntimeTypeHandle elementTypeHandle, b return true; } - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { if (isMdArray && (rank < MDArray.MinRank) && (rank > MDArray.MaxRank)) { @@ -432,7 +432,7 @@ public bool TryGetPointerTypeForTargetType(RuntimeTypeHandle pointeeTypeHandle, if (TryGetPointerTypeForTargetType_LookupOnly(pointeeTypeHandle, out pointerTypeHandle)) return true; - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { if (TypeSystemContext.PointerTypesCache.TryGetValue(pointeeTypeHandle, out pointerTypeHandle)) return true; @@ -461,7 +461,7 @@ public bool TryGetByRefTypeForTargetType(RuntimeTypeHandle pointeeTypeHandle, ou if (TryGetByRefTypeForTargetType_LookupOnly(pointeeTypeHandle, out byRefTypeHandle)) return true; - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { if (TypeSystemContext.ByRefTypesCache.TryGetValue(pointeeTypeHandle, out byRefTypeHandle)) return true; @@ -525,7 +525,7 @@ public bool TryGetGenericMethodDictionaryForComponents(RuntimeTypeHandle declari return true; } - using (LockHolder.Hold(_typeLoaderLock)) + using (_typeLoaderLock.EnterScope()) { bool success = TypeBuilder.TryBuildGenericMethod(methodBeingLoaded, out methodDictionary); diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeSystemContextFactory.cs b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeSystemContextFactory.cs index 4d3fa768fdd2d6..b0175eaaa48034 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeSystemContextFactory.cs +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeSystemContextFactory.cs @@ -22,7 +22,7 @@ public static class TypeSystemContextFactory public static TypeSystemContext Create() { - using (LockHolder.Hold(s_lock)) + using (s_lock.EnterScope()) { TypeSystemContext context = (TypeSystemContext)s_cachedContext.Target; if (context != null) diff --git a/src/coreclr/nativeaot/System.Private.TypeLoader/src/System.Private.TypeLoader.csproj b/src/coreclr/nativeaot/System.Private.TypeLoader/src/System.Private.TypeLoader.csproj index 61799cfc159b1f..b25a5cfcf7d364 100644 --- a/src/coreclr/nativeaot/System.Private.TypeLoader/src/System.Private.TypeLoader.csproj +++ b/src/coreclr/nativeaot/System.Private.TypeLoader/src/System.Private.TypeLoader.csproj @@ -9,6 +9,8 @@ GENERICS_FORCE_USG;$(DefineConstants) false + + true diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Threading.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Threading.cs index 9fe84d213e290c..edcdaf3ee9f852 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Threading.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Threading.cs @@ -11,5 +11,13 @@ internal unsafe partial class Sys [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_CreateThread")] [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool CreateThread(IntPtr stackSize, delegate* unmanaged startAddress, IntPtr parameter); + +#if TARGET_OSX + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetUInt64OSThreadId")] + internal static unsafe partial ulong GetUInt64OSThreadId(); +#else + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_TryGetUInt32OSThreadId")] + internal static unsafe partial uint TryGetUInt32OSThreadId(); +#endif } } diff --git a/src/libraries/System.ComponentModel.Composition.Registration/src/System.ComponentModel.Composition.Registration.csproj b/src/libraries/System.ComponentModel.Composition.Registration/src/System.ComponentModel.Composition.Registration.csproj index b8ae87f03d00b9..66378b9fac149a 100644 --- a/src/libraries/System.ComponentModel.Composition.Registration/src/System.ComponentModel.Composition.Registration.csproj +++ b/src/libraries/System.ComponentModel.Composition.Registration/src/System.ComponentModel.Composition.Registration.csproj @@ -27,8 +27,8 @@ System.ComponentModel.Composition.Registration.ExportBuilder - + diff --git a/src/libraries/System.ComponentModel.Composition.Registration/src/System/ComponentModel/Composition/Registration/RegistrationBuilder.cs b/src/libraries/System.ComponentModel.Composition.Registration/src/System/ComponentModel/Composition/Registration/RegistrationBuilder.cs index c684ababe674eb..5397b13063aa4a 100644 --- a/src/libraries/System.ComponentModel.Composition.Registration/src/System/ComponentModel/Composition/Registration/RegistrationBuilder.cs +++ b/src/libraries/System.ComponentModel.Composition.Registration/src/System/ComponentModel/Composition/Registration/RegistrationBuilder.cs @@ -20,7 +20,7 @@ internal sealed class InnerRC : ReflectionContext private static readonly ReflectionContext s_inner = new InnerRC(); private static readonly List s_emptyList = new List(); - private readonly Lock _lock = new Lock(); + private readonly ReadWriteLock _lock = new ReadWriteLock(); private readonly List _conventions = new List(); private readonly Dictionary> _memberInfos = new Dictionary>(); diff --git a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadLock.cs b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadLock.cs index b60130e6378b5b..40b1dcf7523ee6 100644 --- a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadLock.cs +++ b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadLock.cs @@ -5,10 +5,10 @@ namespace System.Threading { internal struct ReadLock : IDisposable { - private readonly Lock _lock; + private readonly ReadWriteLock _lock; private int _isDisposed; - public ReadLock(Lock @lock) + public ReadLock(ReadWriteLock @lock) { _isDisposed = 0; _lock = @lock; diff --git a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/Lock.cs b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadWriteLock.cs similarity index 94% rename from src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/Lock.cs rename to src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadWriteLock.cs index 8d5101922df7ab..6f02e3db4c5546 100644 --- a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/Lock.cs +++ b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/ReadWriteLock.cs @@ -3,7 +3,7 @@ namespace System.Threading { - internal sealed class Lock : IDisposable + internal sealed class ReadWriteLock : IDisposable { private readonly ReaderWriterLockSlim _thisLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); private int _isDisposed; diff --git a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/WriteLock.cs b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/WriteLock.cs index 0c68d24b956dbc..dbd68c6af36e8a 100644 --- a/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/WriteLock.cs +++ b/src/libraries/System.ComponentModel.Composition.Registration/src/System/Threading/WriteLock.cs @@ -5,10 +5,10 @@ namespace System.Threading { internal struct WriteLock : IDisposable { - private readonly Lock _lock; + private readonly ReadWriteLock _lock; private int _isDisposed; - public WriteLock(Lock @lock) + public WriteLock(ReadWriteLock @lock) { _isDisposed = 0; _lock = @lock; diff --git a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Reader.cs b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Reader.cs index e7a388afa265a4..77f2a6f3892739 100644 --- a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Reader.cs +++ b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Reader.cs @@ -8,10 +8,10 @@ namespace Microsoft.Internal { internal struct ReadLock : IDisposable { - private readonly Lock _lock; + private readonly ReadWriteLock _lock; private int _isDisposed; - public ReadLock(Lock @lock) + public ReadLock(ReadWriteLock @lock) { _isDisposed = 0; _lock = @lock; diff --git a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.cs b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.ReaderWriter.cs similarity index 94% rename from src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.cs rename to src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.ReaderWriter.cs index 044b2dc691c32e..d799d56961992d 100644 --- a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.cs +++ b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.ReaderWriter.cs @@ -6,7 +6,7 @@ namespace Microsoft.Internal { - internal sealed class Lock : IDisposable + internal sealed class ReadWriteLock : IDisposable { private readonly ReaderWriterLockSlim _thisLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); private int _isDisposed; diff --git a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Writer.cs b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Writer.cs index a7137c28a379b4..ca6cfd7abb6bb4 100644 --- a/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Writer.cs +++ b/src/libraries/System.ComponentModel.Composition/src/Microsoft/Internal/Lock.Writer.cs @@ -8,10 +8,10 @@ namespace Microsoft.Internal { internal struct WriteLock : IDisposable { - private readonly Lock _lock; + private readonly ReadWriteLock _lock; private int _isDisposed; - public WriteLock(Lock @lock) + public WriteLock(ReadWriteLock @lock) { _isDisposed = 0; _lock = @lock; diff --git a/src/libraries/System.ComponentModel.Composition/src/System.ComponentModel.Composition.csproj b/src/libraries/System.ComponentModel.Composition/src/System.ComponentModel.Composition.csproj index 0c043fd8a11c8a..aae409d311531d 100644 --- a/src/libraries/System.ComponentModel.Composition/src/System.ComponentModel.Composition.csproj +++ b/src/libraries/System.ComponentModel.Composition/src/System.ComponentModel.Composition.csproj @@ -41,8 +41,8 @@ System.ComponentModel.Composition.ReflectionModel.ReflectionModelServices - + diff --git a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/ComposablePartCatalogCollection.cs b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/ComposablePartCatalogCollection.cs index 810834261e25c0..5264003442603a 100644 --- a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/ComposablePartCatalogCollection.cs +++ b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/ComposablePartCatalogCollection.cs @@ -19,7 +19,7 @@ namespace System.ComponentModel.Composition.Hosting /// internal sealed class ComposablePartCatalogCollection : ICollection, INotifyComposablePartCatalogChanged, IDisposable { - private readonly Lock _lock = new Lock(); + private readonly ReadWriteLock _lock = new ReadWriteLock(); private readonly Action? _onChanged; private readonly Action? _onChanging; private List _catalogs = new List(); diff --git a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/CompositionLock.cs b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/CompositionLock.cs index 0b6de9492e37b4..53ea59b8fab3ef 100644 --- a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/CompositionLock.cs +++ b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/CompositionLock.cs @@ -22,7 +22,7 @@ namespace System.ComponentModel.Composition.Hosting internal sealed class CompositionLock : IDisposable { // narrow lock - private readonly Lock? _stateLock; + private readonly ReadWriteLock? _stateLock; // wide lock private static readonly object _compositionLock = new object(); @@ -36,7 +36,7 @@ public CompositionLock(bool isThreadSafe) _isThreadSafe = isThreadSafe; if (isThreadSafe) { - _stateLock = new Lock(); + _stateLock = new ReadWriteLock(); } } diff --git a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/DirectoryCatalog.cs b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/DirectoryCatalog.cs index 0694fb819d5350..201ccada3e5f53 100644 --- a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/DirectoryCatalog.cs +++ b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/Hosting/DirectoryCatalog.cs @@ -28,7 +28,7 @@ public partial class DirectoryCatalog : ComposablePartCatalog, INotifyComposable RuntimeInformation.IsOSPlatform(OSPlatform.Windows); #endif - private readonly Lock _thisLock = new Lock(); + private readonly ReadWriteLock _thisLock = new ReadWriteLock(); private readonly ICompositionElement? _definitionOrigin; private ComposablePartCatalogCollection _catalogCollection; private Dictionary _assemblyCatalogs; diff --git a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/MetadataViewGenerator.cs b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/MetadataViewGenerator.cs index 47640c87d93875..c4331dc1ec0c54 100644 --- a/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/MetadataViewGenerator.cs +++ b/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/MetadataViewGenerator.cs @@ -65,7 +65,7 @@ internal static class MetadataViewGenerator public const string MetadataItemValue = "MetadataItemValue"; public const string MetadataViewFactoryName = "Create"; - private static readonly Lock _lock = new Lock(); + private static readonly ReadWriteLock _lock = new ReadWriteLock(); private static readonly Dictionary _metadataViewFactories = new Dictionary(); private static readonly AssemblyName ProxyAssemblyName = new AssemblyName($"MetadataViewProxies_{Guid.NewGuid()}"); private static ModuleBuilder? transparentProxyModuleBuilder; diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index b8f8a8980528c7..5f69d9873162dd 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3434,7 +3434,13 @@ Setter must have parameters. - + + The lock has reached the limit of recursive enters. + + + The lock has reached the limit of how many threads may wait for the lock. + + The calling thread does not hold the lock. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index ed3325afbec528..22020416c7431d 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1200,6 +1200,8 @@ + + @@ -1788,8 +1790,8 @@ Common\Interop\Windows\Kernel32\Interop.GetCurrentProcessId.cs - - Interop\Windows\Kernel32\Interop.GetCurrentThreadId.cs + + Common\Interop\Windows\Kernel32\Interop.GetCurrentThreadId.cs Common\Interop\Windows\Kernel32\Interop.GetFileAttributesEx.cs @@ -2402,6 +2404,9 @@ Common\Interop\Unix\System.Native\Interop.SysLog.cs + + Common\Interop\Unix\System.Native\Interop.Threading.cs + Common\Interop\Unix\System.Native\Interop.Unlink.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs index efbe9e691625d7..c16f72184736b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs @@ -7,18 +7,25 @@ namespace System { internal static class AppContextConfigHelper { - internal static bool GetBooleanConfig(string configName, bool defaultValue) => - AppContext.TryGetSwitch(configName, out bool value) ? value : defaultValue; + internal static bool GetBooleanConfig(string switchName, bool defaultValue) => + AppContext.TryGetSwitch(switchName, out bool value) ? value : defaultValue; internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false) { - if (!AppContext.TryGetSwitch(switchName, out bool ret)) + string? str = Environment.GetEnvironmentVariable(envVariable); + if (str != null) { - string? switchValue = Environment.GetEnvironmentVariable(envVariable); - ret = switchValue != null ? (bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1")) : defaultValue; + if (str.Equals("1", StringComparison.Ordinal) || bool.IsTrueStringIgnoreCase(str)) + { + return true; + } + if (str.Equals("0", StringComparison.Ordinal) || bool.IsFalseStringIgnoreCase(str)) + { + return false; + } } - return ret; + return GetBooleanConfig(switchName, defaultValue); } internal static int GetInt32Config(string configName, int defaultValue, bool allowNegative = true) diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.NativeSinks.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.NativeSinks.cs index ad7ee54ae227a4..1cf9b4fecd6dbc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.NativeSinks.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.NativeSinks.cs @@ -83,6 +83,12 @@ private void ContentionLockCreated(nint LockID, nint AssociatedObjectID, ushort LogContentionLockCreated(LockID, AssociatedObjectID, ClrInstanceID); } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) + [NonEvent] + [MethodImpl(MethodImplOptions.NoInlining)] + public void ContentionLockCreated(Lock lockObj) => ContentionLockCreated(lockObj.LockIdForEvents, lockObj.ObjectIdForEvents); +#pragma warning restore CA2252 + [Event(81, Level = EventLevel.Informational, Message = Messages.ContentionStart, Task = Tasks.Contention, Opcode = EventOpcode.Start, Version = 2, Keywords = Keywords.ContentionKeyword)] private void ContentionStart( ContentionFlagsMap ContentionFlags, @@ -95,6 +101,18 @@ private void ContentionStart( LogContentionStart(ContentionFlags, ClrInstanceID, LockID, AssociatedObjectID, LockOwnerThreadID); } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) + [NonEvent] + [MethodImpl(MethodImplOptions.NoInlining)] + public void ContentionStart(Lock lockObj) => + ContentionStart( + ContentionFlagsMap.Managed, + DefaultClrInstanceId, + lockObj.LockIdForEvents, + lockObj.ObjectIdForEvents, + lockObj.OwningThreadId); +#pragma warning restore CA2252 + [Event(91, Level = EventLevel.Informational, Message = Messages.ContentionStop, Task = Tasks.Contention, Opcode = EventOpcode.Stop, Version = 1, Keywords = Keywords.ContentionKeyword)] private void ContentionStop(ContentionFlagsMap ContentionFlags, ushort ClrInstanceID, double DurationNs) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.cs index c7a6d96aab2ba2..668547832733ea 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.cs @@ -95,6 +95,12 @@ private unsafe void ContentionLockCreated(nint LockID, nint AssociatedObjectID, WriteEventCore(90, 3, data); } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) + [NonEvent] + [MethodImpl(MethodImplOptions.NoInlining)] + public void ContentionLockCreated(Lock lockObj) => ContentionLockCreated(lockObj.LockIdForEvents, lockObj.ObjectIdForEvents); +#pragma warning restore CA2252 + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "Parameters to this method are primitive and are trimmer safe")] [Event(81, Level = EventLevel.Informational, Message = Messages.ContentionStart, Task = Tasks.Contention, Opcode = EventOpcode.Start, Version = 2, Keywords = Keywords.ContentionKeyword)] private unsafe void ContentionStart( @@ -125,6 +131,18 @@ private unsafe void ContentionStart( WriteEventCore(81, 3, data); } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) + [NonEvent] + [MethodImpl(MethodImplOptions.NoInlining)] + public void ContentionStart(Lock lockObj) => + ContentionStart( + ContentionFlagsMap.Managed, + DefaultClrInstanceId, + lockObj.LockIdForEvents, + lockObj.ObjectIdForEvents, + lockObj.OwningThreadId); +#pragma warning restore CA2252 + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "Parameters to this method are primitive and are trimmer safe")] [Event(91, Level = EventLevel.Informational, Message = Messages.ContentionStop, Task = Tasks.Contention, Opcode = EventOpcode.Stop, Version = 1, Keywords = Keywords.ContentionKeyword)] private unsafe void ContentionStop(ContentionFlagsMap ContentionFlags, ushort ClrInstanceID, double DurationNs) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.NonNativeAot.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.NonNativeAot.cs new file mode 100644 index 00000000000000..9386b7ed174601 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.NonNativeAot.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Threading +{ + public sealed partial class Lock + { + private static readonly short s_maxSpinCount = DetermineMaxSpinCount(); + private static readonly short s_minSpinCount = DetermineMinSpinCount(); + + /// + /// Initializes a new instance of the class. + /// + public Lock() => _spinCount = s_maxSpinCount; + + private static TryLockResult LazyInitializeOrEnter() => TryLockResult.Spin; + private static bool IsSingleProcessor => Environment.IsSingleProcessor; + + internal partial struct ThreadId + { +#if TARGET_OSX + [ThreadStatic] + private static ulong t_threadId; + + private ulong _id; + + public ThreadId(ulong id) => _id = id; + public ulong Id => _id; +#else + [ThreadStatic] + private static uint t_threadId; + + private uint _id; + + public ThreadId(uint id) => _id = id; + public uint Id => _id; +#endif + + public bool IsInitialized => _id != 0; + public static ThreadId Current_NoInitialize => new ThreadId(t_threadId); + + public void InitializeForCurrentThread() + { + Debug.Assert(!IsInitialized); + Debug.Assert(t_threadId == 0); + +#if TARGET_WINDOWS + uint id = (uint)Interop.Kernel32.GetCurrentThreadId(); +#elif TARGET_OSX + ulong id = Interop.Sys.GetUInt64OSThreadId(); +#else + uint id = Interop.Sys.TryGetUInt32OSThreadId(); + if (id == unchecked((uint)-1)) + { + id = (uint)Environment.CurrentManagedThreadId; + Debug.Assert(id != 0); + } + else +#endif + + if (id == 0) + { + id--; + } + + t_threadId = _id = id; + Debug.Assert(IsInitialized); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs new file mode 100644 index 00000000000000..0d7380d48b5a1e --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs @@ -0,0 +1,1241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace System.Threading +{ + /// + /// Provides a way to get mutual exclusion in regions of code between different threads. A lock may be held by one thread at + /// a time. + /// + /// + /// Threads that cannot immediately enter the lock may wait for the lock to be exited or until a specified timeout. A thread + /// that holds a lock may enter the lock repeatedly without exiting it, such as recursively, in which case the thread should + /// eventually exit the lock the same number of times to fully exit the lock and allow other threads to enter the lock. + /// + [Runtime.Versioning.RequiresPreviewFeatures] + public sealed partial class Lock + { + private const short DefaultMaxSpinCount = 22; + private const short DefaultAdaptiveSpinPeriod = 100; + private const short SpinSleep0Threshold = 10; + private const ushort MaxDurationMsForPreemptingWaiters = 100; + + private static long s_contentionCount; + + // The field's type is not ThreadId to try to retain the relative order of fields of intrinsic types. The type system + // appears to place struct fields after fields of other types, in which case there can be a greater chance that + // _owningThreadId is not in the same cache line as _state. +#if TARGET_OSX && !NATIVEAOT + private ulong _owningThreadId; +#else + private uint _owningThreadId; +#endif + + private uint _state; // see State for layout + private uint _recursionCount; + private short _spinCount; + private ushort _waiterStartTimeMs; + private AutoResetEvent? _waitEvent; + + /// + /// Enters the lock. Once the method returns, the calling thread would be the only thread that holds the lock. + /// + /// + /// If the lock cannot be entered immediately, the calling thread waits for the lock to be exited. If the lock is + /// already held by the calling thread, the lock is entered again. The calling thread should exit the lock as many times + /// as it had entered the lock to fully exit the lock and allow other threads to enter the lock. + /// + /// + /// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high + /// enough that it would typically not be reached when the lock is used properly. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public void Enter() + { + ThreadId currentThreadId = TryEnter_Inlined(timeoutMs: -1); + Debug.Assert(currentThreadId.IsInitialized); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private ThreadId EnterAndGetCurrentThreadId() + { + ThreadId currentThreadId = TryEnter_Inlined(timeoutMs: -1); + Debug.Assert(currentThreadId.IsInitialized); + Debug.Assert(currentThreadId.Id == _owningThreadId); + return currentThreadId; + } + + /// + /// Enters the lock and returns a that may be disposed to exit the lock. Once the method returns, + /// the calling thread would be the only thread that holds the lock. This method is intended to be used along with a + /// language construct that would automatically dispose the , such as with the C# using + /// statement. + /// + /// + /// A that may be disposed to exit the lock. + /// + /// + /// If the lock cannot be entered immediately, the calling thread waits for the lock to be exited. If the lock is + /// already held by the calling thread, the lock is entered again. The calling thread should exit the lock, such as by + /// disposing the returned , as many times as it had entered the lock to fully exit the lock and + /// allow other threads to enter the lock. + /// + /// + /// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high + /// enough that it would typically not be reached when the lock is used properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Scope EnterScope() => new Scope(this, EnterAndGetCurrentThreadId()); + + /// + /// A disposable structure that is returned by , which when disposed, exits the lock. + /// + public ref struct Scope + { + private Lock? _lockObj; + private ThreadId _currentThreadId; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Scope(Lock lockObj, ThreadId currentThreadId) + { + _lockObj = lockObj; + _currentThreadId = currentThreadId; + } + + /// + /// Exits the lock. + /// + /// + /// If the calling thread holds the lock multiple times, such as recursively, the lock is exited only once. The + /// calling thread should ensure that each enter is matched with an exit. + /// + /// + /// The calling thread does not hold the lock. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + Lock? lockObj = _lockObj; + if (lockObj != null) + { + _lockObj = null; + lockObj.Exit(_currentThreadId); + } + } + } + + /// + /// Tries to enter the lock without waiting. If the lock is entered, the calling thread would be the only thread that + /// holds the lock. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// If the lock cannot be entered immediately, the method returns false. If the lock is already held by the + /// calling thread, the lock is entered again. The calling thread should exit the lock as many times as it had entered + /// the lock to fully exit the lock and allow other threads to enter the lock. + /// + /// + /// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high + /// enough that it would typically not be reached when the lock is used properly. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public bool TryEnter() => TryEnter_Inlined(timeoutMs: 0).IsInitialized; + + /// + /// Tries to enter the lock, waiting for roughly the specified duration. If the lock is entered, the calling thread + /// would be the only thread that holds the lock. + /// + /// + /// The rough duration in milliseconds for which the method will wait if the lock is not available. A value of + /// 0 specifies that the method should not wait, and a value of or + /// -1 specifies that the method should wait indefinitely until the lock is entered. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// If the lock cannot be entered immediately, the calling thread waits for roughly the specified duration for the lock + /// to be exited. If the lock is already held by the calling thread, the lock is entered again. The calling thread + /// should exit the lock as many times as it had entered the lock to fully exit the lock and allow other threads to + /// enter the lock. + /// + /// + /// is less than -1. + /// + /// + /// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high + /// enough that it would typically not be reached when the lock is used properly. + /// + public bool TryEnter(int millisecondsTimeout) + { + ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1); + return TryEnter_Outlined(millisecondsTimeout); + } + + /// + /// Tries to enter the lock, waiting for roughly the specified duration. If the lock is entered, the calling thread + /// would be the only thread that holds the lock. + /// + /// + /// The rough duration for which the method will wait if the lock is not available. The timeout is converted to a number + /// of milliseconds by casting of the timeout to an integer value. A value + /// representing 0 milliseconds specifies that the method should not wait, and a value representing + /// or -1 milliseconds specifies that the method should wait indefinitely + /// until the lock is entered. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// If the lock cannot be entered immediately, the calling thread waits for roughly the specified duration for the lock + /// to be exited. If the lock is already held by the calling thread, the lock is entered again. The calling thread + /// should exit the lock as many times as it had entered the lock to fully exit the lock and allow other threads to + /// enter the lock. + /// + /// + /// , after its conversion to an integer millisecond value, represents a value that is less + /// than -1 milliseconds or greater than milliseconds. + /// + /// + /// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high + /// enough that it would typically not be reached when the lock is used properly. + /// + public bool TryEnter(TimeSpan timeout) => TryEnter_Outlined(WaitHandle.ToTimeoutMilliseconds(timeout)); + + [MethodImpl(MethodImplOptions.NoInlining)] + private bool TryEnter_Outlined(int timeoutMs) => TryEnter_Inlined(timeoutMs).IsInitialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ThreadId TryEnter_Inlined(int timeoutMs) + { + Debug.Assert(timeoutMs >= -1); + + ThreadId currentThreadId = ThreadId.Current_NoInitialize; + if (currentThreadId.IsInitialized && State.TryLock(this)) + { + Debug.Assert(!new ThreadId(_owningThreadId).IsInitialized); + Debug.Assert(_recursionCount == 0); + _owningThreadId = currentThreadId.Id; + return currentThreadId; + } + + return TryEnterSlow(timeoutMs, currentThreadId); + } + + /// + /// Exits the lock. + /// + /// + /// If the calling thread holds the lock multiple times, such as recursively, the lock is exited only once. The + /// calling thread should ensure that each enter is matched with an exit. + /// + /// + /// The calling thread does not hold the lock. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public void Exit() + { + var owningThreadId = new ThreadId(_owningThreadId); + if (!owningThreadId.IsInitialized || owningThreadId.Id != ThreadId.Current_NoInitialize.Id) + { + ThrowHelper.ThrowSynchronizationLockException_LockExit(); + } + + ExitImpl(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Exit(ThreadId currentThreadId) + { + Debug.Assert(currentThreadId.IsInitialized); + Debug.Assert(currentThreadId.Id == ThreadId.Current_NoInitialize.Id); + + if (_owningThreadId != currentThreadId.Id) + { + ThrowHelper.ThrowSynchronizationLockException_LockExit(); + } + + ExitImpl(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ExitImpl() + { + Debug.Assert(new ThreadId(_owningThreadId).IsInitialized); + Debug.Assert(_owningThreadId == ThreadId.Current_NoInitialize.Id); + Debug.Assert(new State(this).IsLocked); + + if (_recursionCount == 0) + { + _owningThreadId = 0; + + State state = State.Unlock(this); + if (state.HasAnyWaiters) + { + SignalWaiterIfNecessary(state); + } + } + else + { + _recursionCount--; + } + } + + private static bool IsAdaptiveSpinEnabled(short minSpinCount) => minSpinCount <= 0; + + [MethodImpl(MethodImplOptions.NoInlining)] +#if !NATIVEAOT + private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) +#else + private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId, object associatedObject) +#endif + { + Debug.Assert(timeoutMs >= -1); + + if (!currentThreadId.IsInitialized) + { + // The thread info hasn't been initialized yet for this thread, and the fast path hasn't been tried yet. After + // initializing the thread info, try the fast path first. + currentThreadId.InitializeForCurrentThread(); + Debug.Assert(_owningThreadId != currentThreadId.Id); + if (State.TryLock(this)) + { + goto Locked; + } + } + else if (_owningThreadId == currentThreadId.Id) + { + Debug.Assert(new State(this).IsLocked); + + uint newRecursionCount = _recursionCount + 1; + if (newRecursionCount != 0) + { + _recursionCount = newRecursionCount; + return currentThreadId; + } + + throw new LockRecursionException(SR.Lock_Enter_LockRecursionException); + } + + if (timeoutMs == 0) + { + return new ThreadId(0); + } + + if (LazyInitializeOrEnter() == TryLockResult.Locked) + { + goto Locked; + } + + bool isSingleProcessor = IsSingleProcessor; + short maxSpinCount = s_maxSpinCount; + if (maxSpinCount == 0) + { + goto Wait; + } + + short minSpinCount = s_minSpinCount; + 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)); + _spinCount = (short)(spinCount + 1); + goto Wait; + } + + // Try to acquire the lock, and check if non-waiters should stop preempting waiters. If this thread should not + // preempt waiters, skip spin-waiting. Upon contention, register a spinner. + TryLockResult tryLockResult = State.TryLockBeforeSpinLoop(this, spinCount, out bool isFirstSpinner); + if (tryLockResult != TryLockResult.Spin) + { + goto LockedOrWait; + } + + // Lock was not acquired and a spinner was registered + + if (isFirstSpinner) + { + // Whether a full-length spin-wait would be effective is determined by having the first spinner do a full-length + // spin-wait to see if it is effective. Shorter spin-waits would more often be ineffective just because they are + // shorter. + spinCount = maxSpinCount; + } + + for (short spinIndex = 0; ;) + { + LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor); + + if (++spinIndex >= spinCount) + { + // The last lock attempt for this spin will be done after the loop + break; + } + + // Try to acquire the lock and unregister the spinner + tryLockResult = State.TryLockInsideSpinLoop(this); + if (tryLockResult == TryLockResult.Spin) + { + continue; + } + + if (tryLockResult == TryLockResult.Locked) + { + if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCount)) + { + // 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 + spinCount = _spinCount; + if (spinCount < maxSpinCount) + { + _spinCount = (short)(spinCount + 1); + } + } + + goto Locked; + } + + // The lock was not acquired and the spinner was not unregistered, stop spinning + Debug.Assert(tryLockResult == TryLockResult.Wait); + break; + } + + // Unregister the spinner and try to acquire the lock + tryLockResult = State.TryLockAfterSpinLoop(this); + if (isFirstSpinner && IsAdaptiveSpinEnabled(minSpinCount)) + { + // 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 + if (tryLockResult == TryLockResult.Locked) + { + spinCount = _spinCount; + if (spinCount < maxSpinCount) + { + _spinCount = (short)(spinCount + 1); + } + } + else + { + // If the spin count is already zero, skip spin-waiting for a while, even for the first spinners. After a + // 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; + } + } + + LockedOrWait: + Debug.Assert(tryLockResult != TryLockResult.Spin); + if (tryLockResult == TryLockResult.Wait) + { + goto Wait; + } + + Debug.Assert(tryLockResult == TryLockResult.Locked); + + Locked: + Debug.Assert(!new ThreadId(_owningThreadId).IsInitialized); + Debug.Assert(_recursionCount == 0); + _owningThreadId = currentThreadId.Id; + return currentThreadId; + + Wait: + bool areContentionEventsEnabled = + NativeRuntimeEventSource.Log.IsEnabled( + EventLevel.Informational, + NativeRuntimeEventSource.Keywords.ContentionKeyword); + AutoResetEvent waitEvent = _waitEvent ?? CreateWaitEvent(areContentionEventsEnabled); + if (State.TryLockBeforeWait(this)) + { + // Lock was acquired and a waiter was not registered + goto Locked; + } + + // Lock was not acquired and a waiter was registered. All following paths need to unregister the waiter, including + // exceptional paths. + try + { +#if NATIVEAOT + using var debugBlockingScope = + new Monitor.DebugBlockingScope( + associatedObject, + Monitor.DebugBlockingItemType.MonitorCriticalSection, + timeoutMs, + out _); +#endif + + Interlocked.Increment(ref s_contentionCount); + + long waitStartTimeTicks = 0; + if (areContentionEventsEnabled) + { + NativeRuntimeEventSource.Log.ContentionStart(this); + waitStartTimeTicks = Stopwatch.GetTimestamp(); + } + + bool acquiredLock = false; + int waitStartTimeMs = timeoutMs < 0 ? 0 : Environment.TickCount; + int remainingTimeoutMs = timeoutMs; + while (true) + { + if (!waitEvent.WaitOne(remainingTimeoutMs)) + { + break; + } + + // Spin a bit while trying to acquire the lock. This has a few benefits: + // - Spinning helps to reduce waiter starvation. Since other non-waiter threads can take the lock while + // there are waiters (see State.TryLock()), once a waiter wakes it will be able to better compete with + // other spinners for the lock. + // - If there is another thread that is repeatedly acquiring and releasing the lock, spinning before waiting + // again helps to prevent a waiter from repeatedly context-switching in and out + // - Further in the same situation above, waking up and waiting shortly thereafter deprioritizes this waiter + // because events release waiters in FIFO order. Spinning a bit helps a waiter to retain its priority at + // least for one spin duration before it gets deprioritized behind all other waiters. + for (short spinIndex = 0; spinIndex < maxSpinCount; spinIndex++) + { + if (State.TryLockInsideWaiterSpinLoop(this)) + { + acquiredLock = true; + break; + } + + LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor); + } + + if (acquiredLock) + { + break; + } + + if (State.TryLockAfterWaiterSpinLoop(this)) + { + acquiredLock = true; + break; + } + + if (remainingTimeoutMs < 0) + { + continue; + } + + uint waitDurationMs = (uint)(Environment.TickCount - waitStartTimeMs); + if (waitDurationMs >= (uint)timeoutMs) + { + break; + } + + remainingTimeoutMs = timeoutMs - (int)waitDurationMs; + } + + if (acquiredLock) + { + // In NativeAOT, ensure that class construction cycles do not occur after the lock is acquired but before + // the state is fully updated. Update the state to fully reflect that this thread owns the lock before doing + // other things. + Debug.Assert(!new ThreadId(_owningThreadId).IsInitialized); + Debug.Assert(_recursionCount == 0); + _owningThreadId = currentThreadId.Id; + + if (areContentionEventsEnabled) + { + double waitDurationNs = + (Stopwatch.GetTimestamp() - waitStartTimeTicks) * 1_000_000_000.0 / Stopwatch.Frequency; + NativeRuntimeEventSource.Log.ContentionStop(waitDurationNs); + } + + return currentThreadId; + } + } + catch // run this code before exception filters in callers + { + State.UnregisterWaiter(this); + throw; + } + + State.UnregisterWaiter(this); + return new ThreadId(0); + } + + private void ResetWaiterStartTime() => _waiterStartTimeMs = 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RecordWaiterStartTime() + { + ushort currentTimeMs = (ushort)Environment.TickCount; + if (currentTimeMs == 0) + { + // Don't record zero, that value is reserved for indicating that a time is not recorded + currentTimeMs--; + } + _waiterStartTimeMs = currentTimeMs; + } + + private bool ShouldStopPreemptingWaiters + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // If the recorded time is zero, a time has not been recorded yet + ushort waiterStartTimeMs = _waiterStartTimeMs; + return + waiterStartTimeMs != 0 && + (ushort)Environment.TickCount - waiterStartTimeMs >= MaxDurationMsForPreemptingWaiters; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe AutoResetEvent CreateWaitEvent(bool areContentionEventsEnabled) + { + var newWaitEvent = new AutoResetEvent(false); + AutoResetEvent? waitEventBeforeUpdate = Interlocked.CompareExchange(ref _waitEvent, newWaitEvent, null); + if (waitEventBeforeUpdate == null) + { + // Also check NativeRuntimeEventSource.Log.IsEnabled() to enable trimming + if (areContentionEventsEnabled && NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ContentionLockCreated(this); + } + + return newWaitEvent; + } + + newWaitEvent.Dispose(); + return waitEventBeforeUpdate; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void SignalWaiterIfNecessary(State state) + { + if (State.TrySetIsWaiterSignaledToWake(this, state)) + { + // Signal a waiter to wake + Debug.Assert(_waitEvent != null); + bool signaled = _waitEvent.Set(); + Debug.Assert(signaled); + } + } + + /// + /// true if the lock is held by the calling thread, false otherwise. + /// + public bool IsHeldByCurrentThread + { + get + { + var owningThreadId = new ThreadId(_owningThreadId); + bool isHeld = owningThreadId.IsInitialized && owningThreadId.Id == ThreadId.Current_NoInitialize.Id; + Debug.Assert(!isHeld || new State(this).IsLocked); + return isHeld; + } + } + + internal static long ContentionCount => s_contentionCount; + internal void Dispose() => _waitEvent?.Dispose(); + + internal nint LockIdForEvents + { + get + { + Debug.Assert(_waitEvent != null); + return _waitEvent.SafeWaitHandle.DangerousGetHandle(); + } + } + + internal unsafe nint ObjectIdForEvents + { + get + { + Lock lockObj = this; + return *(nint*)Unsafe.AsPointer(ref lockObj); + } + } + + internal ulong OwningThreadId => _owningThreadId; + + private static short DetermineMaxSpinCount() => + AppContextConfigHelper.GetInt16Config( + "System.Threading.Lock.SpinCount", + "DOTNET_Lock_SpinCount", + DefaultMaxSpinCount, + allowNegative: false); + + private static short DetermineMinSpinCount() + { + // The config var can be set to -1 to disable adaptive spin + short adaptiveSpinPeriod = + AppContextConfigHelper.GetInt16Config( + "System.Threading.Lock.AdaptiveSpinPeriod", + "DOTNET_Lock_AdaptiveSpinPeriod", + DefaultAdaptiveSpinPeriod, + allowNegative: true); + if (adaptiveSpinPeriod < -1) + { + adaptiveSpinPeriod = DefaultAdaptiveSpinPeriod; + } + + return (short)-adaptiveSpinPeriod; + } + + private struct State : IEquatable + { + // Layout constants for Lock._state + private const uint IsLockedMask = (uint)1 << 0; // bit 0 + private const uint ShouldNotPreemptWaitersMask = (uint)1 << 1; // bit 1 + private const uint SpinnerCountIncrement = (uint)1 << 2; // bits 2-4 + private const uint SpinnerCountMask = (uint)0x7 << 2; + private const uint IsWaiterSignaledToWakeMask = (uint)1 << 5; // bit 5 + private const byte WaiterCountShift = 6; + private const uint WaiterCountIncrement = (uint)1 << WaiterCountShift; // bits 6-31 + + private uint _state; + + public State(Lock lockObj) : this(lockObj._state) { } + private State(uint state) => _state = state; + + public static uint InitialStateValue => 0; + public static uint LockedStateValue => IsLockedMask; + private static uint Neg(uint state) => (uint)-(int)state; + public bool IsInitialState => this == default; + public bool IsLocked => (_state & IsLockedMask) != 0; + + private void SetIsLocked() + { + Debug.Assert(!IsLocked); + _state += IsLockedMask; + } + + private bool ShouldNotPreemptWaiters => (_state & ShouldNotPreemptWaitersMask) != 0; + + private void SetShouldNotPreemptWaiters() + { + Debug.Assert(!ShouldNotPreemptWaiters); + Debug.Assert(HasAnyWaiters); + + _state += ShouldNotPreemptWaitersMask; + } + + private void ClearShouldNotPreemptWaiters() + { + Debug.Assert(ShouldNotPreemptWaiters); + _state -= ShouldNotPreemptWaitersMask; + } + + private bool ShouldNonWaiterAttemptToAcquireLock + { + get + { + Debug.Assert(HasAnyWaiters || !ShouldNotPreemptWaiters); + return (_state & (IsLockedMask | ShouldNotPreemptWaitersMask)) == 0; + } + } + + private bool HasAnySpinners => (_state & SpinnerCountMask) != 0; + + private bool TryIncrementSpinnerCount() + { + uint newState = _state + SpinnerCountIncrement; + if (new State(newState).HasAnySpinners) // overflow check + { + _state = newState; + return true; + } + return false; + } + + private void DecrementSpinnerCount() + { + Debug.Assert(HasAnySpinners); + _state -= SpinnerCountIncrement; + } + + private bool IsWaiterSignaledToWake => (_state & IsWaiterSignaledToWakeMask) != 0; + + private void SetIsWaiterSignaledToWake() + { + Debug.Assert(HasAnyWaiters); + Debug.Assert(NeedToSignalWaiter); + + _state += IsWaiterSignaledToWakeMask; + } + + private void ClearIsWaiterSignaledToWake() + { + Debug.Assert(IsWaiterSignaledToWake); + _state -= IsWaiterSignaledToWakeMask; + } + + public bool HasAnyWaiters => _state >= WaiterCountIncrement; + + private bool TryIncrementWaiterCount() + { + uint newState = _state + WaiterCountIncrement; + if (new State(newState).HasAnyWaiters) // overflow check + { + _state = newState; + return true; + } + return false; + } + + private void DecrementWaiterCount() + { + Debug.Assert(HasAnyWaiters); + _state -= WaiterCountIncrement; + } + + public bool NeedToSignalWaiter + { + get + { + Debug.Assert(HasAnyWaiters); + return (_state & (SpinnerCountMask | IsWaiterSignaledToWakeMask)) == 0; + } + } + + public static bool operator ==(State state1, State state2) => state1._state == state2._state; + public static bool operator !=(State state1, State state2) => !(state1 == state2); + + bool IEquatable.Equals(State other) => this == other; + public override bool Equals(object? obj) => obj is State other && this == other; + public override int GetHashCode() => (int)_state; + + private static State CompareExchange(Lock lockObj, State toState, State fromState) => + new State(Interlocked.CompareExchange(ref lockObj._state, toState._state, fromState._state)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryLock(Lock lockObj) + { + // The lock is mostly fair to release waiters in a typically FIFO order (though the order is not guaranteed). + // However, it allows non-waiters to acquire the lock if it's available to avoid lock convoys. + // + // Lock convoys can be detrimental to performance in scenarios where work is being done on multiple threads and + // the work involves periodically taking a particular lock for a short time to access shared resources. With a + // lock convoy, once there is a waiter for the lock (which is not uncommon in such scenarios), a worker thread + // would be forced to context-switch on the subsequent attempt to acquire the lock, often long before the worker + // thread exhausts its time slice. This process repeats as long as the lock has a waiter, forcing every worker + // to context-switch on each attempt to acquire the lock, killing performance and creating a positive feedback + // loop that makes it more likely for the lock to have waiters. To avoid the lock convoy, each worker needs to + // be allowed to acquire the lock multiple times in sequence despite there being a waiter for the lock in order + // to have the worker continue working efficiently during its time slice as long as the lock is not contended. + // + // This scheme has the possibility to starve waiters. Waiter starvation is mitigated by other means, see + // TryLockBeforeSpinLoop() and references to ShouldNotPreemptWaiters. + + var state = new State(lockObj); + if (!state.ShouldNonWaiterAttemptToAcquireLock) + { + return false; + } + + State newState = state; + newState.SetIsLocked(); + + return CompareExchange(lockObj, newState, state) == state; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static State Unlock(Lock lockObj) + { + Debug.Assert(IsLockedMask == 1); + + var state = new State(Interlocked.Decrement(ref lockObj._state)); + Debug.Assert(!state.IsLocked); + return state; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TryLockResult TryLockBeforeSpinLoop(Lock lockObj, short spinCount, out bool isFirstSpinner) + { + // Normally, threads are allowed to preempt waiters to acquire the lock in order to avoid creating lock convoys, + // see TryLock(). There can be cases where waiters can be easily starved as a result. For example, a thread that + // holds a lock for a significant amount of time (much longer than the time it takes to do a context switch), + // then releases and reacquires the lock in quick succession, and repeats. Though a waiter would be woken upon + // lock release, usually it will not have enough time to context-switch-in and take the lock, and can be starved + // for an unreasonably long duration. + // + // In order to prevent such starvation and force a bit of fair forward progress, it is sometimes necessary to + // change the normal policy and disallow threads from preempting waiters. ShouldNotPreemptWaiters() indicates + // the current state of the policy and this method determines whether the policy should be changed to disallow + // non-waiters from preempting waiters. + // - When the first waiter begins waiting, it records the current time as a "waiter starvation start time". + // That is a point in time after which no forward progress has occurred for waiters. When a waiter acquires + // the lock, the time is updated to the current time. + // - This method checks whether the starvation duration has crossed a threshold and if so, sets + // ShouldNotPreemptWaitersMask + // + // When unreasonable starvation is occurring, the lock will be released occasionally and if caused by spinners, + // those threads may start to spin again. + // - Before starting to spin this method is called. If ShouldNotPreemptWaitersMask is set, the spinner will + // skip spinning and wait instead. Spinners that are already registered at the time + // ShouldNotPreemptWaitersMask is set will stop spinning as necessary. Eventually, all spinners will drain + // and no new ones will be registered. + // - Upon releasing a lock, if there are no spinners, a waiter will be signaled to wake. On that path, + // TrySetIsWaiterSignaledToWake() is called. + // - Eventually, after spinners have drained, only a waiter will be able to acquire the lock. When a waiter + // acquires the lock, or when the last waiter unregisters itself, ShouldNotPreemptWaitersMask is cleared to + // restore the normal policy. + + Debug.Assert(spinCount >= 0); + + isFirstSpinner = false; + var state = new State(lockObj); + while (true) + { + State newState = state; + TryLockResult result = TryLockResult.Spin; + if (newState.HasAnyWaiters) + { + if (newState.ShouldNotPreemptWaiters) + { + return TryLockResult.Wait; + } + if (lockObj.ShouldStopPreemptingWaiters) + { + newState.SetShouldNotPreemptWaiters(); + result = TryLockResult.Wait; + } + } + if (result == TryLockResult.Spin) + { + Debug.Assert(!newState.ShouldNotPreemptWaiters); + if (!newState.IsLocked) + { + newState.SetIsLocked(); + result = TryLockResult.Locked; + } + else if ((newState.HasAnySpinners && spinCount == 0) || !newState.TryIncrementSpinnerCount()) + { + return TryLockResult.Wait; + } + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + if (result == TryLockResult.Spin && !state.HasAnySpinners) + { + isFirstSpinner = true; + } + return result; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TryLockResult TryLockInsideSpinLoop(Lock lockObj) + { + // This method is called from inside a spin loop, it must unregister the spinner if the lock is acquired + + var state = new State(lockObj); + while (true) + { + Debug.Assert(state.HasAnySpinners); + if (!state.ShouldNonWaiterAttemptToAcquireLock) + { + return state.ShouldNotPreemptWaiters ? TryLockResult.Wait : TryLockResult.Spin; + } + + State newState = state; + newState.SetIsLocked(); + newState.DecrementSpinnerCount(); + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + return TryLockResult.Locked; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TryLockResult TryLockAfterSpinLoop(Lock lockObj) + { + // This method is called at the end of a spin loop, it must unregister the spinner always and acquire the lock + // if it's available. If the lock is available, a spinner must acquire the lock along with unregistering itself, + // because a lock releaser does not wake a waiter when there is a spinner registered. + + var state = new State(Interlocked.Add(ref lockObj._state, Neg(SpinnerCountIncrement))); + Debug.Assert(new State(state._state + SpinnerCountIncrement).HasAnySpinners); + + while (true) + { + Debug.Assert(state.HasAnyWaiters || !state.ShouldNotPreemptWaiters); + if (state.IsLocked) + { + return TryLockResult.Wait; + } + + State newState = state; + newState.SetIsLocked(); + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + return TryLockResult.Locked; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryLockBeforeWait(Lock lockObj) + { + // This method is called before waiting. It must either acquire the lock or register a waiter. It also keeps + // track of the waiter starvation start time. + + var state = new State(lockObj); + bool waiterStartTimeWasReset = false; + while (true) + { + State newState = state; + if (newState.ShouldNonWaiterAttemptToAcquireLock) + { + newState.SetIsLocked(); + } + else + { + if (!newState.TryIncrementWaiterCount()) + { + ThrowHelper.ThrowOutOfMemoryException_LockEnter_WaiterCountOverflow(); + } + + if (!state.HasAnyWaiters && !waiterStartTimeWasReset) + { + // This would be the first waiter. Once the waiter is registered, another thread may check the + // waiter starvation start time and the previously recorded value may be stale, causing + // ShouldNotPreemptWaitersMask to be set unnecessarily. Reset the start time before registering the + // waiter. + waiterStartTimeWasReset = true; + lockObj.ResetWaiterStartTime(); + } + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + if (state.ShouldNonWaiterAttemptToAcquireLock) + { + return true; + } + + Debug.Assert(state.HasAnyWaiters || waiterStartTimeWasReset); + if (!state.HasAnyWaiters || waiterStartTimeWasReset) + { + // This was the first waiter or the waiter start time was reset, record the waiter start time + lockObj.RecordWaiterStartTime(); + } + return false; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryLockInsideWaiterSpinLoop(Lock lockObj) + { + // This method is called from inside the waiter's spin loop and should observe the wake signal only if the lock + // is taken, to prevent a lock releaser from waking another waiter while one is already spinning to acquire the + // lock + + bool waiterStartTimeWasRecorded = false; + var state = new State(lockObj); + while (true) + { + Debug.Assert(state.HasAnyWaiters); + Debug.Assert(state.IsWaiterSignaledToWake); + + if (state.IsLocked) + { + return false; + } + + State newState = state; + newState.SetIsLocked(); + newState.ClearIsWaiterSignaledToWake(); + newState.DecrementWaiterCount(); + if (newState.ShouldNotPreemptWaiters) + { + newState.ClearShouldNotPreemptWaiters(); + + if (newState.HasAnyWaiters && !waiterStartTimeWasRecorded) + { + // Update the waiter starvation start time. The time must be recorded before + // ShouldNotPreemptWaitersMask is cleared, as once that is cleared, another thread may check the + // waiter starvation start time and the previously recorded value may be stale, causing + // ShouldNotPreemptWaitersMask to be set again unnecessarily. + waiterStartTimeWasRecorded = true; + lockObj.RecordWaiterStartTime(); + } + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + if (newState.HasAnyWaiters) + { + Debug.Assert(!state.ShouldNotPreemptWaiters || waiterStartTimeWasRecorded); + if (!waiterStartTimeWasRecorded) + { + // Since the lock was acquired successfully by a waiter, update the waiter starvation start time + lockObj.RecordWaiterStartTime(); + } + } + return true; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryLockAfterWaiterSpinLoop(Lock lockObj) + { + // This method is called at the end of the waiter's spin loop. It must observe the wake signal always, and if + // the lock is available, it must acquire the lock and unregister the waiter. If the lock is available, a waiter + // must acquire the lock along with observing the wake signal, because a lock releaser does not wake a waiter + // when a waiter was signaled but the wake signal has not been observed. If the lock is acquired, the waiter + // starvation start time is also updated. + + var state = new State(Interlocked.Add(ref lockObj._state, Neg(IsWaiterSignaledToWakeMask))); + Debug.Assert(new State(state._state + IsWaiterSignaledToWakeMask).IsWaiterSignaledToWake); + + bool waiterStartTimeWasRecorded = false; + while (true) + { + Debug.Assert(state.HasAnyWaiters); + + if (state.IsLocked) + { + return false; + } + + State newState = state; + newState.SetIsLocked(); + newState.DecrementWaiterCount(); + if (newState.ShouldNotPreemptWaiters) + { + newState.ClearShouldNotPreemptWaiters(); + + if (newState.HasAnyWaiters && !waiterStartTimeWasRecorded) + { + // Update the waiter starvation start time. The time must be recorded before + // ShouldNotPreemptWaitersMask is cleared, as once that is cleared, another thread may check the + // waiter starvation start time and the previously recorded value may be stale, causing + // ShouldNotPreemptWaitersMask to be set again unnecessarily. + waiterStartTimeWasRecorded = true; + lockObj.RecordWaiterStartTime(); + } + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + if (newState.HasAnyWaiters) + { + Debug.Assert(!state.ShouldNotPreemptWaiters || waiterStartTimeWasRecorded); + if (!waiterStartTimeWasRecorded) + { + // Since the lock was acquired successfully by a waiter, update the waiter starvation start time + lockObj.RecordWaiterStartTime(); + } + } + return true; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void UnregisterWaiter(Lock lockObj) + { + // This method is called upon an exception while waiting, or when a wait has timed out. It must unregister the + // waiter, and if it's the last waiter, clear ShouldNotPreemptWaitersMask to allow other threads to acquire the + // lock. + + var state = new State(lockObj); + while (true) + { + Debug.Assert(state.HasAnyWaiters); + + State newState = state; + newState.DecrementWaiterCount(); + if (newState.ShouldNotPreemptWaiters && !newState.HasAnyWaiters) + { + newState.ClearShouldNotPreemptWaiters(); + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + return; + } + + state = stateBeforeUpdate; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TrySetIsWaiterSignaledToWake(Lock lockObj, State state) + { + // Determine whether we must signal a waiter to wake. Keep track of whether a thread has been signaled to wake + // but has not yet woken from the wait. IsWaiterSignaledToWakeMask is cleared when a signaled thread wakes up by + // observing a signal. Since threads can preempt waiting threads and acquire the lock (see TryLock()), it allows + // for example, one thread to acquire and release the lock multiple times while there are multiple waiting + // threads. In such a case, we don't want that thread to signal a waiter every time it releases the lock, as + // that will cause unnecessary context switches with more and more signaled threads waking up, finding that the + // lock is still locked, and going back into a wait state. So, signal only one waiting thread at a time. + + Debug.Assert(state.HasAnyWaiters); + + while (true) + { + if (!state.NeedToSignalWaiter) + { + return false; + } + + State newState = state; + newState.SetIsWaiterSignaledToWake(); + if (!newState.ShouldNotPreemptWaiters && lockObj.ShouldStopPreemptingWaiters) + { + newState.SetShouldNotPreemptWaiters(); + } + + State stateBeforeUpdate = CompareExchange(lockObj, newState, state); + if (stateBeforeUpdate == state) + { + return true; + } + if (!stateBeforeUpdate.HasAnyWaiters) + { + return false; + } + + state = stateBeforeUpdate; + } + } + } + + private enum TryLockResult + { + Locked, + Spin, + Wait + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs index f555dff7d4eb59..133a14cf1ebe4f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs @@ -78,11 +78,11 @@ public bool Wait(int timeoutMs, bool spinWait) // The PAL's wait subsystem is slower, spin more to compensate for the more expensive wait spinCount *= 2; #endif - int processorCount = Environment.ProcessorCount; - int spinIndex = processorCount > 1 ? 0 : SpinSleep0Threshold; + bool isSingleProcessor = Environment.IsSingleProcessor; + int spinIndex = isSingleProcessor ? SpinSleep0Threshold : 0; while (spinIndex < spinCount) { - LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, processorCount); + LowLevelSpinWaiter.Wait(spinIndex, SpinSleep0Threshold, isSingleProcessor); spinIndex++; // Try to acquire the semaphore and unregister as a spinner diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelSpinWaiter.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelSpinWaiter.cs index ba82f7237e518b..b7a622539c08ce 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelSpinWaiter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelSpinWaiter.cs @@ -34,7 +34,7 @@ public bool SpinWaitForCondition(Func condition, object state, int for (int spinIndex = processorCount > 1 ? 0 : sleep0Threshold; spinIndex < spinCount; ++spinIndex) { // The caller should check the condition in a fast path before calling this method, so wait first - Wait(spinIndex, sleep0Threshold, processorCount); + Wait(spinIndex, sleep0Threshold, processorCount == 1); if (condition(state)) { @@ -51,7 +51,7 @@ public bool SpinWaitForCondition(Func condition, object state, int return false; } - public static void Wait(int spinIndex, int sleep0Threshold, int processorCount) + public static void Wait(int spinIndex, int sleep0Threshold, bool isSingleProcessor) { Debug.Assert(spinIndex >= 0); Debug.Assert(sleep0Threshold >= 0); @@ -65,7 +65,7 @@ public static void Wait(int spinIndex, int sleep0Threshold, int processorCount) // spin loop too early can cause excessive context switcing from the wait. // - If there are multiple threads doing Yield and Sleep(0) (typically from the same spin loop due to contention), // they may switch between one another, delaying work that can make progress. - if (processorCount > 1 && (spinIndex < sleep0Threshold || (spinIndex - sleep0Threshold) % 2 != 0)) + if (!isSingleProcessor && (spinIndex < sleep0Threshold || (spinIndex - sleep0Threshold) % 2 != 0)) { // Cap the maximum spin count to a value such that many thousands of CPU cycles would not be wasted doing // the equivalent of YieldProcessor(), as at that point SwitchToThread/Sleep(0) are more likely to be able to diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs index 68b70dc70ff9d0..c0eba40cb428af 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs @@ -509,7 +509,7 @@ private void ExitSlowPath(bool useMemoryBarrier) bool threadTrackingEnabled = (_owner & LOCK_ID_DISABLE_MASK) == 0; if (threadTrackingEnabled && !IsHeldByCurrentThread) { - throw new SynchronizationLockException(SR.SpinLock_Exit_SynchronizationLockException); + ThrowHelper.ThrowSynchronizationLockException_LockExit(); } if (useMemoryBarrier) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs index 140d136a12cb1f..1f8dadaae8df29 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs @@ -5,7 +5,7 @@ namespace System.Threading { internal sealed partial class TimerQueue { - private static long TickCount64 => Environment.TickCount64; + public static long TickCount64 => Environment.TickCount64; #pragma warning disable IDE0060 private TimerQueue(int id) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs index 48babb49981ef8..566c0cbc10dc1a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs @@ -12,7 +12,7 @@ private TimerQueue(int id) _id = id; } - private static long TickCount64 + public static long TickCount64 { get { diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index 92c09c00b81e9b..27c77960c2f787 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -46,6 +46,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Serialization; +using System.Threading; namespace System { @@ -451,6 +452,12 @@ internal static void ThrowOutOfMemoryException_StringTooLong() throw new OutOfMemoryException(SR.OutOfMemory_StringTooLong); } + [DoesNotReturn] + internal static void ThrowOutOfMemoryException_LockEnter_WaiterCountOverflow() + { + throw new OutOfMemoryException(SR.Lock_Enter_WaiterCountOverflow_OutOfMemoryException); + } + [DoesNotReturn] internal static void ThrowArgumentException_Argument_IncompatibleArrayType() { @@ -613,6 +620,12 @@ internal static void ThrowFormatIndexOutOfRange() throw new FormatException(SR.Format_IndexOutOfRange); } + [DoesNotReturn] + internal static void ThrowSynchronizationLockException_LockExit() + { + throw new SynchronizationLockException(SR.Lock_Exit_SynchronizationLockException); + } + internal static AmbiguousMatchException GetAmbiguousMatchException(MemberInfo memberInfo) { Type? declaringType = memberInfo.DeclaringType; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index e3af0f096e4b62..797b0f52a6c714 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -15068,6 +15068,22 @@ public enum LazyThreadSafetyMode PublicationOnly = 1, ExecutionAndPublication = 2, } + [System.Runtime.Versioning.RequiresPreviewFeaturesAttribute] + public sealed partial class Lock + { + public Lock() { } + public void Enter() { } + public System.Threading.Lock.Scope EnterScope() { throw null; } + public void Exit() { } + public bool IsHeldByCurrentThread { get { throw null; } } + public bool TryEnter() { throw null; } + public bool TryEnter(int millisecondsTimeout) { throw null; } + public bool TryEnter(System.TimeSpan timeout) { throw null; } + public ref struct Scope + { + public void Dispose() { } + } + } public sealed partial class PeriodicTimer : System.IDisposable { public PeriodicTimer(System.TimeSpan period) { } diff --git a/src/libraries/System.Threading/tests/LockTests.cs b/src/libraries/System.Threading/tests/LockTests.cs new file mode 100644 index 00000000000000..b4eab8787c8cac --- /dev/null +++ b/src/libraries/System.Threading/tests/LockTests.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace System.Threading.Tests +{ + public static class LockTests + { + private const int FailTimeoutMilliseconds = 30000; + + // Attempts a single recursive acquisition/release cycle of a newly-created lock. + [Fact] + public static void BasicRecursion() + { + Lock lockObj = new(); + Assert.True(lockObj.TryEnter()); + Assert.True(lockObj.TryEnter()); + lockObj.Exit(); + Assert.True(lockObj.IsHeldByCurrentThread); + lockObj.Enter(); + Assert.True(lockObj.IsHeldByCurrentThread); + lockObj.Exit(); + using (lockObj.EnterScope()) + { + Assert.True(lockObj.IsHeldByCurrentThread); + } + Assert.True(lockObj.IsHeldByCurrentThread); + lockObj.Exit(); + Assert.False(lockObj.IsHeldByCurrentThread); + } + + // Attempts to overflow the recursion count of a newly-created lock. + [Fact] + public static void DeepRecursion() + { + Lock lockObj = new(); + const int successLimit = 10000; + + int i = 0; + for (; i < successLimit; i++) + { + Assert.True(lockObj.TryEnter()); + } + + for (; i > 1; i--) + { + lockObj.Exit(); + Assert.True(lockObj.IsHeldByCurrentThread); + } + + lockObj.Exit(); + Assert.False(lockObj.IsHeldByCurrentThread); + } + + [Fact] + public static void IsHeldByCurrentThread() + { + Lock lockObj = new(); + Assert.False(lockObj.IsHeldByCurrentThread); + using (lockObj.EnterScope()) + { + Assert.True(lockObj.IsHeldByCurrentThread); + } + Assert.False(lockObj.IsHeldByCurrentThread); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public static void IsHeldByCurrentThread_WhenHeldBySomeoneElse() + { + Lock lockObj = new(); + var b = new Barrier(2); + + Task t = Task.Run(() => + { + using (lockObj.EnterScope()) + { + b.SignalAndWait(); + Assert.True(lockObj.IsHeldByCurrentThread); + b.SignalAndWait(); + } + }); + + b.SignalAndWait(); + Assert.False(lockObj.IsHeldByCurrentThread); + b.SignalAndWait(); + + t.Wait(); + } + + [Fact] + public static void Exit_Invalid() + { + Lock lockObj = new(); + Assert.Throws(() => lockObj.Exit()); + default(Lock.Scope).Dispose(); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public static void Exit_WhenHeldBySomeoneElse_ThrowsSynchronizationLockException() + { + Lock lockObj = new(); + var b = new Barrier(2); + + Lock.Scope lockScopeCopy; + using (Lock.Scope lockScope = lockObj.EnterScope()) + { + lockScopeCopy = lockScope; + } + + Task t = Task.Run(() => + { + using (lockObj.EnterScope()) + { + b.SignalAndWait(); + b.SignalAndWait(); + } + }); + + b.SignalAndWait(); + + Assert.Throws(() => lockObj.Exit()); + + try + { + // Can't use Assert.Throws because lockScopeCopy is a ref struct local that can't be captured by a lambda + // expression + lockScopeCopy.Dispose(); + Assert.Fail("Expected SynchronizationLockException but did not get an exception."); + } + catch (SynchronizationLockException) + { + } + catch (Exception ex) + { + Assert.Fail($"Expected SynchronizationLockException but got a different exception instead: {ex}"); + } + + b.SignalAndWait(); + t.Wait(); + } + + [Fact] + public static void TryEnter_Invalid() + { + Lock lockObj = new(); + + Assert.Throws(() => lockObj.TryEnter(-2)); + AssertExtensions.Throws( + "timeout", () => lockObj.TryEnter(TimeSpan.FromMilliseconds(-2))); + AssertExtensions.Throws( + "timeout", + () => lockObj.TryEnter(TimeSpan.FromMilliseconds((double)int.MaxValue + 1))); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public static void Enter_HasToWait() + { + Lock lockObj = new(); + + // When the current thread has the lock, have background threads wait for the lock in various ways. After a short + // duration, release the lock and allow the background threads to acquire the lock. + { + var backgroundTestDelegates = new List(); + Barrier readyBarrier = null; + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + lockObj.Enter(); + lockObj.Exit(); + }); + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + using (lockObj.EnterScope()) + { + } + }); + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + Assert.True(lockObj.TryEnter(ThreadTestHelpers.UnexpectedTimeoutMilliseconds)); + lockObj.Exit(); + }); + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + Assert.True(lockObj.TryEnter(TimeSpan.FromMilliseconds(ThreadTestHelpers.UnexpectedTimeoutMilliseconds))); + lockObj.Exit(); + }); + + int testCount = backgroundTestDelegates.Count; + readyBarrier = new Barrier(testCount + 1); // plus main thread + var waitForThreadArray = new Action[testCount]; + for (int i = 0; i < backgroundTestDelegates.Count; ++i) + { + int icopy = i; // for use in delegates + Thread t = + ThreadTestHelpers.CreateGuardedThread(out waitForThreadArray[i], + () => backgroundTestDelegates[icopy]()); + t.IsBackground = true; + t.Start(); + } + + using (lockObj.EnterScope()) + { + readyBarrier.SignalAndWait(ThreadTestHelpers.UnexpectedTimeoutMilliseconds); + Thread.Sleep(ThreadTestHelpers.ExpectedTimeoutMilliseconds); + } + foreach (Action waitForThread in waitForThreadArray) + waitForThread(); + } + + // When the current thread has the lock, have background threads wait for the lock in various ways and time out + // after a short duration + { + var backgroundTestDelegates = new List(); + Barrier readyBarrier = null; + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + Assert.False(lockObj.TryEnter(ThreadTestHelpers.ExpectedTimeoutMilliseconds)); + }); + + backgroundTestDelegates.Add(() => + { + readyBarrier.SignalAndWait(); + Assert.False(lockObj.TryEnter(TimeSpan.FromMilliseconds(ThreadTestHelpers.ExpectedTimeoutMilliseconds))); + }); + + int testCount = backgroundTestDelegates.Count; + readyBarrier = new Barrier(testCount + 1); // plus main thread + var waitForThreadArray = new Action[testCount]; + for (int i = 0; i < backgroundTestDelegates.Count; ++i) + { + int icopy = i; // for use in delegates + Thread t = + ThreadTestHelpers.CreateGuardedThread(out waitForThreadArray[i], + () => backgroundTestDelegates[icopy]()); + t.IsBackground = true; + t.Start(); + } + + using (lockObj.EnterScope()) + { + readyBarrier.SignalAndWait(ThreadTestHelpers.UnexpectedTimeoutMilliseconds); + foreach (Action waitForThread in waitForThreadArray) + waitForThread(); + } + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public static void Enter_HasToWait_LockContentionCountTest() + { + long initialLockContentionCount = Monitor.LockContentionCount; + Enter_HasToWait(); + Assert.True(Monitor.LockContentionCount - initialLockContentionCount >= 2); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] + public static void InterruptWaitTest() + { + Lock lockObj = new(); + using (lockObj.EnterScope()) + { + var threadReady = new AutoResetEvent(false); + var t = + ThreadTestHelpers.CreateGuardedThread(out Action waitForThread, () => + { + threadReady.Set(); + Assert.Throws(() => lockObj.Enter()); + }); + t.IsBackground = true; + t.Start(); + threadReady.CheckedWait(); + t.Interrupt(); + waitForThread(); + } + } + } +} diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs index 50074a1e241096..c37ed60e5af670 100644 --- a/src/libraries/System.Threading/tests/MonitorTests.cs +++ b/src/libraries/System.Threading/tests/MonitorTests.cs @@ -66,7 +66,7 @@ public static void IsEntered() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/91538", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] - public static void IsEntered_WhenHeldBySomeoneElse_ThrowsSynchronizationLockException() + public static void IsEntered_WhenHeldBySomeoneElse() { var obj = new object(); var b = new Barrier(2); @@ -491,5 +491,29 @@ public static void ObjectHeaderSyncBlockTransitionTryEnterRaceTest() } while (!t.Join(0)); } } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/87718", TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] + public static void InterruptWaitTest() + { + object obj = new(); + lock (obj) + { + var threadReady = new AutoResetEvent(false); + var t = + ThreadTestHelpers.CreateGuardedThread(out Action waitForThread, () => + { + threadReady.Set(); + Assert.Throws(() => Monitor.Enter(obj)); + }); + t.IsBackground = true; + t.Start(); + threadReady.CheckedWait(); + t.Interrupt(); + waitForThread(); + } + } } } diff --git a/src/libraries/System.Threading/tests/System.Threading.Tests.csproj b/src/libraries/System.Threading/tests/System.Threading.Tests.csproj index c4bb4f6191835e..e938db9863da3d 100644 --- a/src/libraries/System.Threading/tests/System.Threading.Tests.csproj +++ b/src/libraries/System.Threading/tests/System.Threading.Tests.csproj @@ -4,6 +4,8 @@ true $(NetCoreAppCurrent) true + + true @@ -14,9 +16,10 @@ - + + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs index 96424ec2bffa9a..fe81b79959f090 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs @@ -145,10 +145,11 @@ private static void ReliableEnterTimeout(object obj, int timeout, ref bool lockT try_enter_with_atomic_var(obj, timeout, true, ref lockTaken); } - public static extern long LockContentionCount - { - [MethodImplAttribute(MethodImplOptions.InternalCall)] - get; - } +#pragma warning disable CA2252 // Opt in to preview features before using them (Lock) + public static long LockContentionCount => Monitor_get_lock_contention_count() + Lock.ContentionCount; +#pragma warning restore CA2252 + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + private static extern long Monitor_get_lock_contention_count(); } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs index f4c7f64f89fe1c..9d9a422e56f7b2 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs @@ -16,7 +16,7 @@ namespace System.Threading // internal partial class TimerQueue { - private static long TickCount64 => Environment.TickCount64; + public static long TickCount64 => Environment.TickCount64; private static List? s_scheduledTimers; private static List? s_scheduledTimersToFire; private static long s_shortestDueTimeMs = long.MaxValue; diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 0c5300aeb38d61..b18d285c421ca0 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -589,10 +589,10 @@ NOHANDLES(ICALL(LIFOSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLe ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject)) HANDLES(MONIT_1, "InternalExit", mono_monitor_exit_icall, void, 1, (MonoObject)) +NOHANDLES(ICALL(MONIT_8, "Monitor_get_lock_contention_count", ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count)) HANDLES(MONIT_2, "Monitor_pulse", ves_icall_System_Threading_Monitor_Monitor_pulse, void, 1, (MonoObject)) HANDLES(MONIT_3, "Monitor_pulse_all", ves_icall_System_Threading_Monitor_Monitor_pulse_all, void, 1, (MonoObject)) HANDLES(MONIT_7, "Monitor_wait", ves_icall_System_Threading_Monitor_Monitor_wait, MonoBoolean, 3, (MonoObject, guint32, MonoBoolean)) -NOHANDLES(ICALL(MONIT_8, "get_LockContentionCount", ves_icall_System_Threading_Monitor_Monitor_LockContentionCount)) HANDLES(MONIT_9, "try_enter_with_atomic_var", ves_icall_System_Threading_Monitor_Monitor_try_enter_with_atomic_var, void, 4, (MonoObject, guint32, MonoBoolean, MonoBoolean_ref)) ICALL_TYPE(THREAD, "System.Threading.Thread", THREAD_1) diff --git a/src/mono/mono/metadata/monitor.c b/src/mono/mono/metadata/monitor.c index 1e861f8dec1b8e..6a72695e9fbedc 100644 --- a/src/mono/mono/metadata/monitor.c +++ b/src/mono/mono/metadata/monitor.c @@ -1434,7 +1434,7 @@ ves_icall_System_Threading_Monitor_Monitor_Enter (MonoObjectHandle obj, MonoErro } gint64 -ves_icall_System_Threading_Monitor_Monitor_LockContentionCount (void) +ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count (void) { return thread_contentions; } diff --git a/src/mono/mono/metadata/monitor.h b/src/mono/mono/metadata/monitor.h index f9be43d681f77b..d1900a889976da 100644 --- a/src/mono/mono/metadata/monitor.h +++ b/src/mono/mono/metadata/monitor.h @@ -129,7 +129,7 @@ mono_monitor_threads_sync_members_offset (int *status_offset, int *nest_offset); ICALL_EXPORT gint64 -ves_icall_System_Threading_Monitor_Monitor_LockContentionCount (void); +ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count (void); #ifdef HOST_WASM void diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 8491ab83c4681b..f4fcd6f8d4d86c 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -275,6 +275,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_GetEnviron) DllImportEntry(SystemNative_FreeEnviron) DllImportEntry(SystemNative_GetGroupName) + DllImportEntry(SystemNative_GetUInt64OSThreadId) + DllImportEntry(SystemNative_TryGetUInt32OSThreadId) }; EXTERN_C const void* SystemResolveDllImport(const char* name); diff --git a/src/native/libs/System.Native/pal_threading.c b/src/native/libs/System.Native/pal_threading.c index 6b2f0883632ac3..c96c7e8abd558c 100644 --- a/src/native/libs/System.Native/pal_threading.c +++ b/src/native/libs/System.Native/pal_threading.c @@ -277,3 +277,51 @@ void SystemNative_Abort(void) { abort(); } + +// Gets a non-truncated OS thread ID that is also suitable for diagnostics, for platforms that offer a 64-bit ID +uint64_t SystemNative_GetUInt64OSThreadId(void) +{ +#ifdef __APPLE__ + uint64_t threadId; + int result = pthread_threadid_np(pthread_self(), &threadId); + assert(result == 0); + return threadId; +#else + assert(false); + return 0; +#endif +} + +#if defined(__linux__) +#include +#include +#elif defined(__FreeBSD__) +#include +#elif defined(__NetBSD__) +#include +#endif + +// Tries to get a non-truncated OS thread ID that is also suitable for diagnostics, for platforms that offer a 32-bit ID. +// Returns (uint32_t)-1 when the implementation does not know how to get the OS thread ID. +uint32_t SystemNative_TryGetUInt32OSThreadId(void) +{ + const uint32_t InvalidId = (uint32_t)-1; + +#if defined(__linux__) + assert(sizeof(pid_t) == sizeof(uint32_t)); + uint32_t threadId = (uint32_t)syscall(SYS_gettid); + assert(threadId != InvalidId); + return threadId; +#elif defined(__FreeBSD__) + uint32_t threadId = (uint32_t)pthread_getthreadid_np(); + assert(threadId != InvalidId); + return threadId; +#elif defined(__NetBSD__) + assert(sizeof(lwpid_t) == sizeof(uint32_t)); + uint32_t threadId = (uint32_t)_lwp_self(); + assert(threadId != InvalidId); + return threadId; +#else + return InvalidId; +#endif +} diff --git a/src/native/libs/System.Native/pal_threading.h b/src/native/libs/System.Native/pal_threading.h index a3b336acd0461b..fb79aaf5928751 100644 --- a/src/native/libs/System.Native/pal_threading.h +++ b/src/native/libs/System.Native/pal_threading.h @@ -29,3 +29,6 @@ PALEXPORT int32_t SystemNative_SchedGetCpu(void); PALEXPORT __attribute__((noreturn)) void SystemNative_Exit(int32_t exitCode); PALEXPORT __attribute__((noreturn)) void SystemNative_Abort(void); + +PALEXPORT uint64_t SystemNative_GetUInt64OSThreadId(void); +PALEXPORT uint32_t SystemNative_TryGetUInt32OSThreadId(void); diff --git a/src/native/libs/System.Native/pal_threading_wasi.c b/src/native/libs/System.Native/pal_threading_wasi.c index 1f82d256ef7ac8..83fec6be55073b 100644 --- a/src/native/libs/System.Native/pal_threading_wasi.c +++ b/src/native/libs/System.Native/pal_threading_wasi.c @@ -80,3 +80,17 @@ void SystemNative_Abort(void) { abort(); } + +// Gets a non-truncated OS thread ID that is also suitable for diagnostics, for platforms that offer a 64-bit ID +uint64_t SystemNative_GetUInt64OSThreadId(void) +{ + assert(false); + return 0; +} + +// Tries to get a non-truncated OS thread ID that is also suitable for diagnostics, for platforms that offer a 32-bit ID. +// Returns (uint32_t)-1 when the implementation does not know how to get the OS thread ID. +uint32_t SystemNative_TryGetUInt32OSThreadId(void) +{ + return (uint32_t)-1; +}