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 ba8aba61a0a1b..2321c5bafe14b 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 @@ -32,6 +32,8 @@ private static Waiter GetWaiterForCurrentThread() private Waiter? _waitersHead; private Waiter? _waitersTail; + internal Lock AssociatedLock => _lock; + private unsafe void AssertIsInList(Waiter waiter) { Debug.Assert(_waitersHead != null && _waitersTail != null); @@ -106,6 +108,8 @@ public unsafe bool Wait(int millisecondsTimeout, object? associatedObjectForMoni if (!_lock.IsHeldByCurrentThread) throw new SynchronizationLockException(); + using ThreadBlockingInfo.Scope threadBlockingScope = new(this, millisecondsTimeout); + Waiter waiter = GetWaiterForCurrentThread(); AddWaiter(waiter); 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 index 8a1c017a50809..9ed8dafac4c1b 100644 --- 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 @@ -24,6 +24,12 @@ public sealed partial class Lock /// public Lock() => _spinCount = SpinCountNotInitialized; +#pragma warning disable CA1822 // can be marked as static - varies between runtimes + internal ulong OwningOSThreadId => 0; +#pragma warning restore CA1822 + + internal int OwningManagedThreadId => (int)_owningThreadId; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryEnterOneShot(int currentManagedThreadId) { diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 59a4783648e2a..54d395c1d2e35 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -1158,6 +1158,13 @@ void SystemDomain::Init() // Finish loading CoreLib now. m_pSystemAssembly->GetDomainAssembly()->EnsureActive(); + + // Set AwareLock's offset of the holding OS thread ID field into ThreadBlockingInfo's static field. That can be used + // when doing managed debugging to get the OS ID of the thread holding the lock. The offset is currently not zero, and + // zero is used in managed code to determine if the static variable has been initialized. + _ASSERTE(AwareLock::GetOffsetOfHoldingOSThreadId() != 0); + CoreLibBinder::GetField(FIELD__THREAD_BLOCKING_INFO__OFFSET_OF_LOCK_OWNER_OS_THREAD_ID) + ->SetStaticValue32(AwareLock::GetOffsetOfHoldingOSThreadId()); } #ifdef _DEBUG diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 65a187d19216f..d5af8bdb3cb64 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -583,6 +583,10 @@ END_ILLINK_FEATURE_SWITCH() DEFINE_CLASS(MONITOR, Threading, Monitor) DEFINE_METHOD(MONITOR, ENTER, Enter, SM_Obj_RetVoid) +DEFINE_CLASS(THREAD_BLOCKING_INFO, Threading, ThreadBlockingInfo) +DEFINE_FIELD(THREAD_BLOCKING_INFO, OFFSET_OF_LOCK_OWNER_OS_THREAD_ID, s_monitorObjectOffsetOfLockOwnerOSThreadId) +DEFINE_FIELD(THREAD_BLOCKING_INFO, FIRST, t_first) + DEFINE_CLASS(PARAMETER, Reflection, ParameterInfo) DEFINE_CLASS(PARAMETER_MODIFIER, Reflection, ParameterModifier) diff --git a/src/coreclr/vm/syncblk.cpp b/src/coreclr/vm/syncblk.cpp index 48ede10b35fef..606137ee8eba7 100644 --- a/src/coreclr/vm/syncblk.cpp +++ b/src/coreclr/vm/syncblk.cpp @@ -2851,15 +2851,9 @@ BOOL SyncBlock::Wait(INT32 timeOut) _ASSERTE ((SyncBlock*)((DWORD_PTR)walk->m_Next->m_WaitSB & ~1)== this); - PendingSync syncState(walk); - - OBJECTREF obj = m_Monitor.GetOwningObject(); - syncState.m_Object = OBJECTREFToObject(obj); - - m_Monitor.IncrementTransientPrecious(); - // While we are in this frame the thread is considered blocked on the - // event of the monitor lock according to the debugger + // event of the monitor lock according to the debugger. DebugBlockingItemHolder + // can trigger a GC, so set it up before accessing the owning object. DebugBlockingItem blockingMonitorInfo; blockingMonitorInfo.dwTimeout = timeOut; blockingMonitorInfo.pMonitor = &m_Monitor; @@ -2867,6 +2861,13 @@ BOOL SyncBlock::Wait(INT32 timeOut) blockingMonitorInfo.type = DebugBlock_MonitorEvent; DebugBlockingItemHolder holder(pCurThread, &blockingMonitorInfo); + PendingSync syncState(walk); + + OBJECTREF obj = m_Monitor.GetOwningObject(); + syncState.m_Object = OBJECTREFToObject(obj); + + m_Monitor.IncrementTransientPrecious(); + GCPROTECT_BEGIN(obj); { GCX_PREEMP(); diff --git a/src/coreclr/vm/syncblk.h b/src/coreclr/vm/syncblk.h index 2ddec75315975..029ee9337d7aa 100644 --- a/src/coreclr/vm/syncblk.h +++ b/src/coreclr/vm/syncblk.h @@ -602,6 +602,12 @@ class AwareLock LIMITED_METHOD_CONTRACT; return m_HoldingThread; } + + static int GetOffsetOfHoldingOSThreadId() + { + LIMITED_METHOD_CONTRACT; + return (int)offsetof(AwareLock, m_HoldingOSThreadId); + } }; #ifdef FEATURE_COMINTEROP diff --git a/src/coreclr/vm/threaddebugblockinginfo.cpp b/src/coreclr/vm/threaddebugblockinginfo.cpp index 8767091566891..8f4f3831b3518 100644 --- a/src/coreclr/vm/threaddebugblockinginfo.cpp +++ b/src/coreclr/vm/threaddebugblockinginfo.cpp @@ -72,9 +72,37 @@ VOID ThreadDebugBlockingInfo::VisitBlockingItems(DebugBlockingItemVisitor visito // Holder constructor pushes a blocking item on the blocking info stack #ifndef DACCESS_COMPILE DebugBlockingItemHolder::DebugBlockingItemHolder(Thread *pThread, DebugBlockingItem *pItem) : -m_pThread(pThread) + m_pThread(pThread), m_ppFirstBlockingInfo(nullptr) { - LIMITED_METHOD_CONTRACT; + CONTRACTL + { + THROWS; + GC_TRIGGERS; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + // Try to get the address of the thread-local slot for the managed ThreadBlockingInfo.t_first + EX_TRY + { + FieldDesc *pFD = CoreLibBinder::GetField(FIELD__THREAD_BLOCKING_INFO__FIRST); + m_ppFirstBlockingInfo = (ThreadBlockingInfo **)Thread::GetStaticFieldAddress(pFD); + } + EX_CATCH + { + } + EX_END_CATCH(RethrowTerminalExceptions); + + if (m_ppFirstBlockingInfo != nullptr) + { + // Push info for the managed ThreadBlockingInfo + m_blockingInfo.objectPtr = pItem->pMonitor; + m_blockingInfo.objectKind = (ThreadBlockingInfo::ObjectKind)pItem->type; + m_blockingInfo.timeoutMs = (INT32)pItem->dwTimeout; + m_blockingInfo.next = *m_ppFirstBlockingInfo; + *m_ppFirstBlockingInfo = &m_blockingInfo; + } + pThread->DebugBlockingInfo.PushBlockingItem(pItem); } #endif //DACCESS_COMPILE @@ -84,6 +112,17 @@ m_pThread(pThread) DebugBlockingItemHolder::~DebugBlockingItemHolder() { LIMITED_METHOD_CONTRACT; + m_pThread->DebugBlockingInfo.PopBlockingItem(); + + if (m_ppFirstBlockingInfo != nullptr) + { + // Pop info for the managed ThreadBlockingInfo + _ASSERTE( + m_ppFirstBlockingInfo == + (void *)m_pThread->GetStaticFieldAddrNoCreate(CoreLibBinder::GetField(FIELD__THREAD_BLOCKING_INFO__FIRST))); + _ASSERTE(*m_ppFirstBlockingInfo == &m_blockingInfo); + *m_ppFirstBlockingInfo = m_blockingInfo.next; + } } #endif //DACCESS_COMPILE diff --git a/src/coreclr/vm/threaddebugblockinginfo.h b/src/coreclr/vm/threaddebugblockinginfo.h index 9a2815b3a0c78..c0d035dd88f01 100644 --- a/src/coreclr/vm/threaddebugblockinginfo.h +++ b/src/coreclr/vm/threaddebugblockinginfo.h @@ -14,8 +14,8 @@ // Different ways thread can block that the debugger will expose enum DebugBlockingItemType { - DebugBlock_MonitorCriticalSection, - DebugBlock_MonitorEvent, + DebugBlock_MonitorCriticalSection, // maps to ThreadBlockingInfo.ObjectKind.MonitorLock below and in managed code + DebugBlock_MonitorEvent, // maps to ThreadBlockingInfo.ObjectKind.MonitorWait below and in managed code }; typedef DPTR(struct DebugBlockingItem) PTR_DebugBlockingItem; @@ -65,15 +65,35 @@ class ThreadDebugBlockingInfo }; #ifndef DACCESS_COMPILE + +// This is the equivalent of the managed ThreadBlockingInfo (see ThreadBlockingInfo.cs), which is used for tracking blocking +// info from the managed side, similarly to DebugBlockingItem +struct ThreadBlockingInfo +{ + enum class ObjectKind : INT32 + { + MonitorLock, // maps to DebugBlockingItemType::DebugBlock_MonitorCriticalSection + MonitorWait // maps to DebugBlockingItemType::DebugBlock_MonitorEvent + }; + + void *objectPtr; + ObjectKind objectKind; + INT32 timeoutMs; + ThreadBlockingInfo *next; +}; + class DebugBlockingItemHolder { private: Thread *m_pThread; + ThreadBlockingInfo **m_ppFirstBlockingInfo; + ThreadBlockingInfo m_blockingInfo; public: DebugBlockingItemHolder(Thread *pThread, DebugBlockingItem *pItem); ~DebugBlockingItemHolder(); }; + #endif //!DACCESS_COMPILE #endif // __ThreadBlockingInfo__ 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 4dd47c701586c..fccffa63fa159 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 @@ -1265,6 +1265,7 @@ + @@ -2786,4 +2787,4 @@ - \ No newline at end of file + 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 index 9386b7ed17460..8a05f1e7fddc7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.NonNativeAot.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.NonNativeAot.cs @@ -16,6 +16,12 @@ public sealed partial class Lock /// public Lock() => _spinCount = s_maxSpinCount; + internal ulong OwningOSThreadId => _owningThreadId; + +#pragma warning disable CA1822 // can be marked as static - varies between runtimes + internal int OwningManagedThreadId => 0; +#pragma warning restore CA1822 + private static TryLockResult LazyInitializeOrEnter() => TryLockResult.Spin; private static bool IsSingleProcessor => Environment.IsSingleProcessor; diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs index b7961869eac5d..8658ab6caafb1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs @@ -477,6 +477,8 @@ private ThreadId TryEnterSlow(int timeoutMs, ThreadId currentThreadId) waitStartTimeTicks = Stopwatch.GetTimestamp(); } + using ThreadBlockingInfo.Scope threadBlockingScope = new(this, timeoutMs); + bool acquiredLock = false; int waitStartTimeMs = timeoutMs < 0 ? 0 : Environment.TickCount; int remainingTimeoutMs = timeoutMs; diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadBlockingInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadBlockingInfo.cs new file mode 100644 index 0000000000000..6deed17e8f5dc --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadBlockingInfo.cs @@ -0,0 +1,194 @@ +// 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; +using System.Runtime.InteropServices; + +namespace System.Threading +{ + // Tracks some kinds of blocking on the thread, like waiting on locks where it may be useful to know when debugging which + // thread owns the lock. + // + // Notes: + // - The type, some fields, and some other members may be used by debuggers (noted specifically below), so take care when + // renaming them + // - There is a native version of this struct in CoreCLR, used by Monitor to fold in its blocking info here. The struct is + // blittable with sequential layout to support that. + // + // Debuggers may use this info by evaluating expressions to enumerate the blocking infos for a thread. For example: + // - Evaluate "System.Threading.ThreadBlockingInfo.t_first" to obtain the first pointer to a blocking info for the current + // thread + // - While there is a non-null pointer to a blocking info: + // - Evaluate "(*(System.Threading.ThreadBlockingInfo*)ptr).fieldOrProperty", where "ptr" is the blocking info pointer + // value, to get the field and relevant property getter values below + // - Use the _objectKind field value to determine what kind of blocking is occurring + // - Get the LockOwnerOSThreadId and LockOwnerManagedThreadId property getter values. If the blocking is waiting for a + // lock and the lock is currently owned by a thread, one of these properties will return a nonzero value that can be + // used to identify the lock owner thread. + // - Use the _next field value to obtain the next pointer to a blocking info for the thread + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct ThreadBlockingInfo + { +#if CORECLR + // In CoreCLR, for the Monitor object kinds, the object ptr will be a pointer to a native AwareLock object. This + // relative offset indicates the location of the field holding the lock owner OS thread ID (the field is of type + // size_t), and is used to get that info by the LockOwnerOSThreadId property. The offset is not zero currently, so zero + // is used to determine if the static field has been initialized. + // + // This mechanism is used instead of using an FCall in the property getter such that the property can be more easily + // evaluated by a debugger. + private static int s_monitorObjectOffsetOfLockOwnerOSThreadId; +#endif + + // Points to the first (most recent) blocking info for the thread. The _next field points to the next-most-recent + // blocking info for the thread, or null if there are no more. Blocking can be reentrant in some cases, such as on UI + // threads where reentrant waits are used, or if a SynchronizationContext wait override is set. + [ThreadStatic] + private static ThreadBlockingInfo* t_first; // may be used by debuggers + + // This pointer can be used to obtain the object relevant to the blocking. For native object kinds, it points to the + // native object (for Monitor object kinds in CoreCLR, it points to a native AwareLock object). For managed object + // kinds, it points to a stack location containing the managed object reference. + private void* _objectPtr; // may be used by debuggers + + // Indicates the type of object relevant to the blocking + private ObjectKind _objectKind; // may be used by debuggers + + // The timeout in milliseconds for the wait, -1 for infinite timeout + private int _timeoutMs; // may be used by debuggers + + // Points to the next-most-recent blocking info for the thread + private ThreadBlockingInfo* _next; // may be used by debuggers + + private void Push(void* objectPtr, ObjectKind objectKind, int timeoutMs) + { + Debug.Assert(objectPtr != null); + + _objectPtr = objectPtr; + _objectKind = objectKind; + _timeoutMs = timeoutMs; + _next = t_first; + t_first = (ThreadBlockingInfo*)Unsafe.AsPointer(ref this); + } + + private void Pop() + { + Debug.Assert(_objectPtr != null); + Debug.Assert(t_first != null); + Debug.Assert(t_first->_next == _next); + + t_first = _next; + _objectPtr = null; + } + + // If the blocking is associated with a lock of some kind that has thread affinity and tracks the owner's OS thread ID, + // returns the OS thread ID of the thread that currently owns the lock. Otherwise, returns 0. A return value of 0 may + // indicate that the associated lock is currently not owned by a thread, or that the information could not be + // determined. + // + // Calls to native helpers are avoided in the property getter such that it can be more easily evaluated by a debugger. + public ulong LockOwnerOSThreadId // the getter may be used by debuggers + { + get + { + Debug.Assert(_objectPtr != null); + + switch (_objectKind) + { + case ObjectKind.MonitorLock: + case ObjectKind.MonitorWait: + // The Monitor object kinds are only used by CoreCLR, and only the OS thread ID is reported +#if CORECLR + if (s_monitorObjectOffsetOfLockOwnerOSThreadId != 0) + { + return *(nuint*)((nint)_objectPtr + s_monitorObjectOffsetOfLockOwnerOSThreadId); + } +#endif + return 0; + + case ObjectKind.Lock: + return ((Lock)Unsafe.AsRef(_objectPtr)).OwningOSThreadId; + + default: + Debug.Assert(_objectKind == ObjectKind.Condition); +#if NATIVEAOT + return ((Condition)Unsafe.AsRef(_objectPtr)).AssociatedLock.OwningOSThreadId; +#else + return 0; +#endif + } + } + } + + // If the blocking is associated with a lock of some kind that has thread affinity and tracks the owner's managed thread + // ID, returns the managed thread ID of the thread that currently owns the lock. Otherwise, returns 0. A return value of + // 0 may indicate that the associated lock is currently not owned by a thread, or that the information could not be + // determined. + // + // Calls to native helpers are avoided in the property getter such that it can be more easily evaluated by a debugger. + public int LockOwnerManagedThreadId // the getter may be used by debuggers + { + get + { + Debug.Assert(_objectPtr != null); + + switch (_objectKind) + { + case ObjectKind.MonitorLock: + case ObjectKind.MonitorWait: + // The Monitor object kinds are only used by CoreCLR, and only the OS thread ID is reported + return 0; + + case ObjectKind.Lock: + return ((Lock)Unsafe.AsRef(_objectPtr)).OwningManagedThreadId; + + default: + Debug.Assert(_objectKind == ObjectKind.Condition); +#if NATIVEAOT + return ((Condition)Unsafe.AsRef(_objectPtr)).AssociatedLock.OwningManagedThreadId; +#else + return 0; +#endif + } + } + } + + public unsafe ref struct Scope + { + private object? _object; + private ThreadBlockingInfo _blockingInfo; + +#pragma warning disable CS9216 // casting Lock to object + public Scope(Lock lockObj, int timeoutMs) : this(lockObj, ObjectKind.Lock, timeoutMs) { } +#pragma warning restore CS9216 + +#if NATIVEAOT + public Scope(Condition condition, int timeoutMs) : this(condition, ObjectKind.Condition, timeoutMs) { } +#endif + + private Scope(object obj, ObjectKind objectKind, int timeoutMs) + { + _object = obj; + _blockingInfo.Push(Unsafe.AsPointer(ref _object), objectKind, timeoutMs); + } + + public void Dispose() + { + if (_object is not null) + { + _blockingInfo.Pop(); + _object = null; + } + } + } + + public enum ObjectKind // may be used by debuggers + { + MonitorLock, // maps to DebugBlockingItemType::DebugBlock_MonitorCriticalSection in coreclr + MonitorWait, // maps to DebugBlockingItemType::DebugBlock_MonitorEvent in coreclr + Lock, + Condition + } + } +}