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