Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ static Lock GetLockObjectFallback(object obj)
#region Public Enter/Exit methods
public static void Enter(object obj)
{
ObjectHeader.HeaderLockResult result = ObjectHeader.TryAcquireThinLock(obj);
ArgumentNullException.ThrowIfNull(obj);
ObjectHeader.HeaderLockResult result = ObjectHeader.Acquire(obj);
if (result == ObjectHeader.HeaderLockResult.Success)
return;

Expand All @@ -65,7 +66,8 @@ public static void Enter(object obj)

public static bool TryEnter(object obj)
{
ObjectHeader.HeaderLockResult result = ObjectHeader.TryAcquireThinLock(obj);
ArgumentNullException.ThrowIfNull(obj);
ObjectHeader.HeaderLockResult result = ObjectHeader.Acquire(obj);
if (result == ObjectHeader.HeaderLockResult.Success)
return true;

Expand All @@ -78,8 +80,9 @@ public static bool TryEnter(object obj)
public static bool TryEnter(object obj, int millisecondsTimeout)
{
ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1);
ArgumentNullException.ThrowIfNull(obj);

ObjectHeader.HeaderLockResult result = ObjectHeader.TryAcquireThinLock(obj);
ObjectHeader.HeaderLockResult result = ObjectHeader.Acquire(obj);
if (result == ObjectHeader.HeaderLockResult.Success)
return true;

Expand All @@ -105,10 +108,9 @@ public static void Exit(object obj)
GetLockObject(obj).Exit();
}

// Marked no-inlining to prevent recursive inlining of IsAcquired.
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsEntered(object obj)
{
ArgumentNullException.ThrowIfNull(obj);
ObjectHeader.HeaderLockResult result = ObjectHeader.IsAcquired(obj);
if (result == ObjectHeader.HeaderLockResult.Success)
return true;
Expand All @@ -122,7 +124,8 @@ public static bool IsEntered(object obj)

internal static void SynchronizedMethodEnter(object obj, ref bool lockTaken)
{
ObjectHeader.HeaderLockResult result = ObjectHeader.TryAcquireThinLock(obj);
ArgumentNullException.ThrowIfNull(obj);
ObjectHeader.HeaderLockResult result = ObjectHeader.Acquire(obj);
if (result == ObjectHeader.HeaderLockResult.Success)
{
lockTaken = true;
Expand All @@ -135,6 +138,7 @@ internal static void SynchronizedMethodEnter(object obj, ref bool lockTaken)

internal static void SynchronizedMethodExit(object obj, ref bool lockTaken)
{
Debug.Assert(obj is not null);
// Inlined Monitor.Exit with a few tweaks
if (!lockTaken)
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,6 @@ namespace System.Threading
/// </remarks>
internal static class ObjectHeader
{
// The following three header bits are reserved for the GC engine:
// BIT_SBLK_UNUSED = 0x80000000
// BIT_SBLK_FINALIZER_RUN = 0x40000000
// BIT_SBLK_GC_RESERVE = 0x20000000
//
// All other bits may be used to store runtime data: hash code, sync entry index, etc.
// Here we use the same bit layout as in CLR: if bit 26 (BIT_SBLK_IS_HASHCODE) is set,
// all the lower bits 0..25 store the hash code, otherwise they store either the sync
// entry index (indicated by BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX) or thin lock data.
private const int IS_HASHCODE_BIT_NUMBER = 26;
private const int IS_HASH_OR_SYNCBLKINDEX_BIT_NUMBER = 27;
private const int BIT_SBLK_IS_HASHCODE = 1 << IS_HASHCODE_BIT_NUMBER;
internal const int MASK_HASHCODE_INDEX = BIT_SBLK_IS_HASHCODE - 1;
private const int BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX = 1 << IS_HASH_OR_SYNCBLKINDEX_BIT_NUMBER;

// This lock is only taken when we need to modify the index value in m_SyncBlockValue.
// It should not be taken if the object already has a real syncblock index.
// In this managed side, we skip the fast path while this spinlock is in use.
// We'll sync up in the slow path.
private const int BIT_SBLK_SPIN_LOCK = 0x10000000;

// if BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX is clear, the rest of the header dword is laid out as follows:
// - lower sixteen bits (bits 0 thru 15) is thread id used for the thin locks
// value is zero if no thread is holding the lock
// - following six bits (bits 16 thru 21) is recursion level used for the thin locks
// value is zero if lock is not taken or only taken once by the same thread
private const int SBLK_MASK_LOCK_THREADID = 0x0000FFFF; // special value of 0 + 65535 thread ids
private const int SBLK_MASK_LOCK_RECLEVEL = 0x003F0000; // 64 recursion levels
private const int SBLK_LOCK_RECLEVEL_INC = 0x00010000; // each level is this much higher than the previous one

// These must match the values in syncblk.h
public enum HeaderLockResult
{
Expand All @@ -57,178 +27,12 @@ public enum HeaderLockResult
};

[MethodImpl(MethodImplOptions.InternalCall)]
private static extern HeaderLockResult AcquireInternal(object obj);
public static extern HeaderLockResult Acquire(object obj);

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern HeaderLockResult Release(object obj);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe int* GetHeaderPtr(byte* ppObjectData)
{
// The header is the 4 bytes before a pointer-sized chunk before the object data pointer.
return (int*)(ppObjectData - sizeof(void*) - sizeof(int));
}

//
// A few words about spinning choices:
//
// Most locks are not contentious. In fact most locks provide exclusive access safety, but in reality are used by
// one thread. And then there are locks that do see multiple threads, but the accesses are short and not overlapping.
// Thin lock is an optimization for such scenarios.
//
// If we see a thin lock held by other thread for longer than ~5 microseconds, we will "inflate" the lock
// and let the adaptive spinning in the fat Lock sort it out whether we have a contentious lock or a long-held lock.
//
// Another thing to consider is that SpinWait(1) is calibrated to about 35-50 nanoseconds.
// It can take much longer only if nop/pause takes much longer, which it should not, as that would be getting
// close to the RAM latency.
//
// Considering that taking and releasing the lock takes 2 CAS instructions + some overhead, we can estimate shortest
// time the lock can be held to be in hundreds of nanoseconds. Thus it is unlikely to see more than
// 8-10 threads contending for the lock without inflating it. Therefore we can expect to acquire a thin lock in
// under 16 tries.
//
// As for the backoff strategy we have two choices:
// Exponential back-off with a lmit:
// 0, 1, 2, 4, 8, 8, 8, 8, 8, 8, 8, . . . .
//
// Linear back-off
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, . . . .
//
// In this case these strategies are close in terms of average and worst case latency, so we will prefer linear
// back-off as it favors micro-contention scenario, which we expect.
//

// Try acquiring the thin-lock
public static unsafe HeaderLockResult TryAcquireThinLock(object obj)
{
ArgumentNullException.ThrowIfNull(obj);

HeaderLockResult result = AcquireInternal(obj);
if (result == HeaderLockResult.Failure)
{
return TryAcquireThinLockSpin(obj);
}
return result;
}

private static unsafe HeaderLockResult TryAcquireThinLockSpin(object obj)
{
int currentThreadID = ManagedThreadId.Current;

// does thread ID fit?
if (currentThreadID > SBLK_MASK_LOCK_THREADID)
return HeaderLockResult.UseSlowPath;

int retries = Lock.IsSingleProcessor ? 0 : 16;

// retry when the lock is owned by somebody else.
// this loop will spinwait between iterations.
for (int i = 0; i <= retries; i++)
{
fixed (byte* pObjectData = &obj.GetRawData())
{
int* pHeader = GetHeaderPtr(pObjectData);

// rare retries when lock is not owned by somebody else.
// these do not count as iterations and do not spinwait.
while (true)
{
int oldBits = *pHeader;

// If has a hash code, syncblock, or is in the process of upgrading,
// we cannot use a thin-lock.
if ((oldBits & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_SPIN_LOCK)) != 0)
{
// Need to use a thick-lock.
return HeaderLockResult.UseSlowPath;
}
// If we already own the lock, try incrementing recursion level.
else if ((oldBits & SBLK_MASK_LOCK_THREADID) == currentThreadID)
{
// try incrementing recursion level, check for overflow
int newBits = oldBits + SBLK_LOCK_RECLEVEL_INC;
if ((newBits & SBLK_MASK_LOCK_RECLEVEL) != 0)
{
if (Interlocked.CompareExchange(ref *pHeader, newBits, oldBits) == oldBits)
{
return HeaderLockResult.Success;
}

// rare contention on owned lock,
// perhaps hashcode was installed or finalization bits were touched.
// we still own the lock though and may be able to increment, try again
continue;
}
else
{
// overflow, need to transition to a fat Lock
return HeaderLockResult.UseSlowPath;
}
}
// If no one owns the lock, try acquiring it.
else if ((oldBits & SBLK_MASK_LOCK_THREADID) == 0)
{
int newBits = oldBits | currentThreadID;
if (Interlocked.CompareExchange(ref *pHeader, newBits, oldBits) == oldBits)
{
return HeaderLockResult.Success;
}

// rare contention on lock.
// Try again in case the finalization bits were touched.
continue;
}
else
{
// Owned by somebody else. Now we spinwait and retry.
break;
}
}
}

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

// owned by somebody else
return HeaderLockResult.Failure;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe HeaderLockResult IsAcquired(object obj)
{
ArgumentNullException.ThrowIfNull(obj);

fixed (byte* pObjectData = &obj.GetRawData())
{
int* pHeader = GetHeaderPtr(pObjectData);

// Ignore the spinlock here.
// Either we'll read the thin-lock data in the header or we'll have a sync block.
// In either case, the two will be consistent.
int oldBits = *pHeader;

// If has a hash code or syncblock, we cannot determine the lock state from the header
// use the slow path.
if ((oldBits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX) != 0)
{
return HeaderLockResult.UseSlowPath;
}

// if we own the lock
if ((oldBits & SBLK_MASK_LOCK_THREADID) == ManagedThreadId.Current)
{
return HeaderLockResult.Success;
}

// someone else owns or no one.
return HeaderLockResult.Failure;
}
}
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern HeaderLockResult IsAcquired(object obj);
}
}
8 changes: 8 additions & 0 deletions src/coreclr/vm/comsynchronizable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,14 @@ FCIMPL1(ObjHeader::HeaderLockResult, ObjHeader_ReleaseThinLock, Object* obj)
}
FCIMPLEND

FCIMPL1(ObjHeader::HeaderLockResult, ObjHeader_IsThinLockOwnedByThread, Object* obj)
{
FCALL_CONTRACT;

return obj->GetHeader()->IsHeaderThinLockOwnedByThread(GetThread()->GetThreadId());
}
FCIMPLEND

extern "C" INT32 QCALLTYPE ThreadNative_ReentrantWaitAny(BOOL alertable, INT32 timeout, INT32 count, HANDLE *handles)
{
QCALL_CONTRACT;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/comsynchronizable.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ FCDECL1(OBJECTHANDLE, Monitor_GetLockHandleIfExists, Object* obj);

FCDECL1(ObjHeader::HeaderLockResult, ObjHeader_AcquireThinLock, Object* obj);
FCDECL1(ObjHeader::HeaderLockResult, ObjHeader_ReleaseThinLock, Object* obj);
FCDECL1(ObjHeader::HeaderLockResult, ObjHeader_IsThinLockOwnedByThread, Object* obj);
#endif // _COMSYNCHRONIZABLE_H

3 changes: 2 additions & 1 deletion src/coreclr/vm/ecalllist.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ FCFuncStart(gThreadFuncs)
FCFuncEnd()

FCFuncStart(gObjectHeaderFuncs)
FCFuncElement("AcquireInternal", ObjHeader_AcquireThinLock)
FCFuncElement("Acquire", ObjHeader_AcquireThinLock)
FCFuncElement("Release", ObjHeader_ReleaseThinLock)
FCFuncElement("IsAcquired", ObjHeader_IsThinLockOwnedByThread)
FCFuncEnd()

FCFuncStart(gMonitorFuncs)
Expand Down
Loading
Loading