Skip to content

Commit 84d9854

Browse files
authored
Refactor lock upgrade logic (#122571)
1 parent a274767 commit 84d9854

File tree

7 files changed

+62
-23
lines changed

7 files changed

+62
-23
lines changed

src/coreclr/System.Private.CoreLib/src/System/Threading/Interlocked.CoreCLR.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
67
using System.Runtime.InteropServices;
@@ -127,6 +128,26 @@ public static int CompareExchange(ref int location1, int value, int comparand)
127128
[MethodImpl(MethodImplOptions.InternalCall)]
128129
private static extern int CompareExchange32(ref int location1, int value, int comparand);
129130

131+
// This is used internally in cases where having a managed
132+
// ref to the location is unsafe (Ex: it is the object header of a pinned object).
133+
// The intrinsic expansion for this overload is exactly the same as for the `ref int`
134+
// variant and will go on the same path since expansion is triggered by the name and
135+
// return type of the method.
136+
// The important part is avoiding `ref *location` that is reported as byref to the GC.
137+
[Intrinsic]
138+
internal static unsafe int CompareExchange(int* location1, int value, int comparand)
139+
{
140+
#if TARGET_X86 || TARGET_AMD64 || TARGET_ARM64 || TARGET_RISCV64
141+
return CompareExchange(location1, value, comparand); // Must expand intrinsic
142+
#else
143+
Debug.Assert(location1 != null);
144+
return CompareExchange32Pointer(location1, value, comparand);
145+
#endif
146+
}
147+
148+
[MethodImpl(MethodImplOptions.InternalCall)]
149+
private static unsafe extern int CompareExchange32Pointer(int* location1, int value, int comparand);
150+
130151
/// <summary>Compares two 64-bit signed integers for equality and, if they are equal, replaces the first value.</summary>
131152
/// <param name="location1">The destination, whose value is compared with <paramref name="comparand"/> and possibly replaced.</param>
132153
/// <param name="value">The value that replaces the destination value if the comparison results in equality.</param>

src/coreclr/System.Private.CoreLib/src/System/Threading/ObjectHeader.CoreCLR.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ private static unsafe HeaderLockResult TryAcquireThinLockSpin(object obj)
150150
int newBits = oldBits + SBLK_LOCK_RECLEVEL_INC;
151151
if ((newBits & SBLK_MASK_LOCK_RECLEVEL) != 0)
152152
{
153-
if (Interlocked.CompareExchange(ref *pHeader, newBits, oldBits) == oldBits)
153+
if (Interlocked.CompareExchange(pHeader, newBits, oldBits) == oldBits)
154154
{
155155
return HeaderLockResult.Success;
156156
}
@@ -170,7 +170,7 @@ private static unsafe HeaderLockResult TryAcquireThinLockSpin(object obj)
170170
else if ((oldBits & SBLK_MASK_LOCK_THREADID) == 0)
171171
{
172172
int newBits = oldBits | currentThreadID;
173-
if (Interlocked.CompareExchange(ref *pHeader, newBits, oldBits) == oldBits)
173+
if (Interlocked.CompareExchange(pHeader, newBits, oldBits) == oldBits)
174174
{
175175
return HeaderLockResult.Success;
176176
}

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Interlocked.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ public static int CompareExchange(ref int location1, int value, int comparand)
2424
#endif
2525
}
2626

27-
// This is used internally by NativeAOT runtime in cases where having a managed
28-
// ref to the location is unsafe (Ex: it is the syncblock of a pinned object).
27+
// This is used internally in cases where having a managed
28+
// ref to the location is unsafe (Ex: it is the object header of a pinned object).
2929
// The intrinsic expansion for this overload is exactly the same as for the `ref int`
3030
// variant and will go on the same path since expansion is triggered by the name and
3131
// return type of the method.
32-
// The important part is avoiding `ref *location` in the unexpanded scenario, like
33-
// in a case when compiling the Debug flavor of the app.
32+
// The important part is avoiding `ref *location` that is reported as byref to the GC.
3433
[Intrinsic]
3534
internal static unsafe int CompareExchange(int* location1, int value, int comparand)
3635
{

src/coreclr/vm/ecalllist.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ FCFuncStart(gInterlockedFuncs)
323323
FCFuncElement("Exchange64", COMInterlocked::Exchange64)
324324
FCFuncElement("ExchangeObject", COMInterlocked::ExchangeObject)
325325
FCFuncElement("CompareExchange32", COMInterlocked::CompareExchange32)
326+
FCFuncElement("CompareExchange32Pointer", COMInterlocked::CompareExchange32)
326327
FCFuncElement("CompareExchange64", COMInterlocked::CompareExchange64)
327328
FCFuncElement("CompareExchangeObject", COMInterlocked::CompareExchangeObject)
328329
FCFuncElement("ExchangeAdd32", COMInterlocked::ExchangeAdd32)

src/coreclr/vm/syncblk.cpp

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,6 +1737,25 @@ OBJECTHANDLE SyncBlock::GetOrCreateLock(OBJECTREF lockObj)
17371737
// Create the handle here.
17381738
OBJECTHANDLEHolder lockHandle = GetAppDomain()->CreateHandle(lockObj);
17391739

1740+
if (TryUpgradeThinLockToFullLock(lockHandle.GetValue()))
1741+
{
1742+
// Our lock instance is the one in the sync block now.
1743+
return lockHandle.Extract();
1744+
}
1745+
1746+
return VolatileLoad(&m_Lock);
1747+
}
1748+
1749+
bool SyncBlock::TryUpgradeThinLockToFullLock(OBJECTHANDLE lockHandle)
1750+
{
1751+
CONTRACTL
1752+
{
1753+
GC_TRIGGERS;
1754+
THROWS;
1755+
MODE_COOPERATIVE;
1756+
}
1757+
CONTRACTL_END;
1758+
17401759
// Switch to preemptive so we can grab the spin-lock.
17411760
// Use the NO_DTOR version so we don't do a coop->preemptive->coop transition on return.
17421761
GCX_PREEMP_NO_DTOR();
@@ -1747,10 +1766,9 @@ OBJECTHANDLE SyncBlock::GetOrCreateLock(OBJECTREF lockObj)
17471766
GCX_PREEMP_NO_DTOR_END();
17481767

17491768
// Check again now that we hold the spin-lock
1750-
existingLock = m_Lock;
1751-
if (existingLock != (OBJECTHANDLE)NULL)
1769+
if (m_Lock != (OBJECTHANDLE)NULL)
17521770
{
1753-
return existingLock;
1771+
return false;
17541772
}
17551773

17561774
// We need to create a new lock
@@ -1761,32 +1779,31 @@ OBJECTHANDLE SyncBlock::GetOrCreateLock(OBJECTREF lockObj)
17611779

17621780
if (thinLock != 0)
17631781
{
1764-
1765-
// We have thin-lock info that needs to be transferred to the lock object.
17661782
DWORD lockThreadId = thinLock & SBLK_MASK_LOCK_THREADID;
17671783
DWORD recursionLevel = (thinLock & SBLK_MASK_LOCK_RECLEVEL) >> SBLK_RECLEVEL_SHIFT;
17681784
_ASSERTE(lockThreadId != 0);
17691785
PREPARE_NONVIRTUAL_CALLSITE(METHOD__LOCK__INITIALIZE_FOR_MONITOR);
1786+
1787+
// We have thin-lock info that needs to be transferred to the lock object.
1788+
OBJECTREF lockObj = ObjectFromHandle(lockHandle);
1789+
17701790
DECLARE_ARGHOLDER_ARRAY(args, 3);
1771-
args[ARGNUM_0] = OBJECTREF_TO_ARGHOLDER(ObjectFromHandle(lockHandle));
1791+
args[ARGNUM_0] = OBJECTREF_TO_ARGHOLDER(lockObj);
17721792
args[ARGNUM_1] = DWORD_TO_ARGHOLDER(lockThreadId);
17731793
args[ARGNUM_2] = DWORD_TO_ARGHOLDER(recursionLevel);
17741794
CALL_MANAGED_METHOD_NORET(args);
17751795
}
17761796

1777-
VolatileStore(&m_Lock, lockHandle.GetValue());
1778-
1779-
// Our lock instance is in the sync block now.
1780-
// Don't release it.
1781-
lockHandle.SuppressRelease();
1782-
1783-
// Also, clear the thin lock info.
1797+
VolatileStore(&m_Lock, lockHandle);
1798+
// Clear the thin lock info.
17841799
// It won't be used any more, but it will look out of date.
17851800
// Only clear the relevant bits, as the spin-lock bit is used to lock this method.
17861801
// That bit will be reset upon return.
17871802
m_thinLock.StoreWithoutBarrier(m_thinLock.LoadWithoutBarrier() & ~((SBLK_MASK_LOCK_THREADID) | (SBLK_MASK_LOCK_RECLEVEL)));
17881803

1789-
return lockHandle;
1804+
// Our lock instance is in the sync block now.
1805+
// Don't release it.
1806+
return true;
17901807
}
17911808
#endif // !DACCESS_COMPILE
17921809

src/coreclr/vm/syncblk.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,8 @@ class SyncBlock
592592
private:
593593
void InitializeThinLock(DWORD recursionLevel, DWORD threadId);
594594

595+
bool TryUpgradeThinLockToFullLock(OBJECTHANDLE lockHandle);
596+
595597
friend struct ::cdac_data<SyncBlock>;
596598
};
597599

src/coreclr/vm/syncblk.inl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ FORCEINLINE ObjHeader::HeaderLockResult ObjHeader::AcquireHeaderThinLock(DWORD t
7474
return HeaderLockResult::Success;
7575
}
7676

77-
// Use the slow path instead of spinning. The compare-exchange above would not fail often, and it's not worth forcing the
78-
// spin loop that typically follows the call to this function to check the recursive case, so just bail to the slow path.
79-
return HeaderLockResult::UseSlowPath;
77+
// We failed to acquire because someone touched other bits in the header.
78+
return HeaderLockResult::Failure;
8079
}
8180

8281
// Helper encapsulating the core logic for releasing monitor. Returns what kind of

0 commit comments

Comments
 (0)