| File: System\Threading\SyncTable.cs | Web Access |
| Project: src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib) |
// 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; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Threading { /// <summary> /// Stores the hash code and the Monitor synchronization object for all managed objects used /// with the Monitor.Enter/TryEnter/Exit methods. /// </summary> /// <remarks> /// This implementation is faster than ConditionalWeakTable since we store the synchronization /// entry index in the object header, which avoids a hash table lookup. It closely matches /// implementation of SyncBlocks. /// /// SyncTable assigns a unique table entry to each object it is asked for. The assigned entry /// index is stored in the object header and preserved during table expansion (we never shrink /// the table). Each table entry contains a long weak GC handle representing the owner object /// of that entry and may be in one of the three states: /// 1) Free (IsAllocated == false). These entries have been either never used or used and /// freed/recycled after their owners died. We keep a linked list of recycled entries and /// use it to dispense entries to new objects. /// 2) Live (Target != null). These entries store the hash code and the Monitor synchronization /// object assigned to Target. /// 3) Dead (Target == null). These entries lost their owners and are ready to be freed/recycled. /// /// Here is the state diagram for an entry: /// Free --{AssignEntry}--> Live --{GC}--> Dead --{(Recycle|Free)DeadEntries} --> Free /// /// The public methods operates on live entries only and acquire the following locks: /// * GetLockObject : Lock-free. We always allocate a Monitor synchronization object before /// the entry goes live. The returned object may be used as normal; no /// additional synchronization required. /// * GetHashCode : Lock-free. A stale zero value may be returned. /// * SetHashCode : Acquires s_lock. /// * AssignEntry : Acquires s_lock. /// /// The important part here is that all read operations are lock-free and fast, and write /// operations are expected to be much less frequent than read ones. /// /// </remarks> [EagerStaticClassConstruction] internal static class SyncTable { /// <summary> /// The initial size of the table. Must be positive and not greater than /// ObjectHeader.MASK_HASHCODE_INDEX + 1. /// </summary> #if DEBUG // Exercise table expansion more frequently in debug builds private const int InitialSize = 1; #else private const int InitialSize = 1 << 7; #endif /// <summary> /// The table size threshold for doubling in size. Must be positive. /// </summary> private const int DoublingSizeThreshold = 1 << 20; /// <summary> /// Protects all mutable operations on s_entries, s_freeEntryList, s_unusedEntryIndex. Also protects growing the table. /// </summary> internal static readonly Lock s_lock = new Lock(useTrivialWaits: true); /// <summary> /// The dynamically growing array of sync entries. /// </summary> private static Entry[] s_entries = new Entry[InitialSize]; /// <summary> /// The head of the list of freed entries linked using the Next property. /// </summary> private static int s_freeEntryList; /// <summary> /// The index of the lowest never used entry. We skip the 0th entry and start with 1. /// If all entries have been used, s_unusedEntryIndex == s_entries.Length. This counter /// never decreases. /// </summary> private static int s_unusedEntryIndex = 1; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ref Entry UnsafeEntryRef(int i) { #if DEBUG return ref s_entries[i]; #else return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(s_entries), i); #endif } /// <summary> /// Assigns a sync table entry to the object in a thread-safe way. /// </summary> public static unsafe int AssignEntry(object obj, int* pHeader) { // Allocate the synchronization object outside the lock Lock lck = new Lock(); DeadEntryCollector collector = new DeadEntryCollector(); DependentHandle handle = new DependentHandle(obj, collector); try { using (s_lock.EnterScope()) { // After acquiring the lock check whether another thread already assigned the sync entry if (ObjectHeader.GetSyncEntryIndex(*pHeader, out int syncIndex)) { return syncIndex; } if (s_freeEntryList != 0) { // Grab a free entry from the list syncIndex = s_freeEntryList; ref Entry freeEntry = ref s_entries[syncIndex]; s_freeEntryList = freeEntry.Next; freeEntry.Next = 0; } else { if (s_unusedEntryIndex >= s_entries.Length) { // No free entries, use the slow path. This call may OOM. Grow(); } // Grab the next unused entry Debug.Assert(s_unusedEntryIndex < s_entries.Length); syncIndex = s_unusedEntryIndex++; } ref Entry entry = ref s_entries[syncIndex]; // Found a free entry to assign Debug.Assert(!entry.Owner.IsAllocated); Debug.Assert(entry.Lock is null); Debug.Assert(entry.HashCode == 0); // Set up the new entry. We should not fail after this point. entry.Lock = lck; // The hash code will be set by the SetSyncEntryIndex call below entry.Owner = handle; handle = default; collector.Activate(syncIndex); collector = default!; // Finally, store the entry index in the object header ObjectHeader.SetSyncEntryIndex(pHeader, syncIndex); return syncIndex; } } finally { if (collector != null) GC.SuppressFinalize(collector); handle.Dispose(); } } /// <summary> /// Grows the sync table. If memory is not available, it throws an OOM exception keeping /// the state valid. /// </summary> private static void Grow() { Debug.Assert(s_lock.IsHeldByCurrentThread); int oldSize = s_entries.Length; int newSize = CalculateNewSize(oldSize); Entry[] newEntries = new Entry[newSize]; // Copy the shallow content of the table Array.Copy(s_entries, newEntries, oldSize); // Publish the new table. Lock-free reader threads must not see the new value of // s_entries until all the content is copied to the new table. Volatile.Write(ref s_entries, newEntries); } /// <summary> /// Calculates the new size of the sync table if it needs to grow. Throws an OOM exception /// in case of size overflow. /// </summary> private static int CalculateNewSize(int oldSize) { Debug.Assert(oldSize > 0); Debug.Assert(ObjectHeader.MASK_HASHCODE_INDEX < int.MaxValue); int newSize; if (oldSize <= DoublingSizeThreshold) { // Double in size; overflow is checked below newSize = unchecked(oldSize * 2); } else { // For bigger tables use a smaller factor 1.5 Debug.Assert(oldSize > 1); newSize = unchecked(oldSize + (oldSize >> 1)); } // All indices must fit in the mask, limit the size accordingly newSize = Math.Min(newSize, ObjectHeader.MASK_HASHCODE_INDEX + 1); // Make sure the new size has not overflowed and is actually bigger if (newSize <= oldSize) { throw new OutOfMemoryException(); } return newSize; } /// <summary> /// Returns the stored hash code. The zero value indicates the hash code has not yet been /// assigned or visible to this thread. /// </summary> public static int GetHashCode(int syncIndex) { // This thread may be looking at an old version of s_entries. If the old version had // no hash code stored, GetHashCode returns zero and the subsequent SetHashCode call // will resolve the potential race. return UnsafeEntryRef(syncIndex).HashCode; } /// <summary> /// Sets the hash code in a thread-safe way. /// </summary> public static int SetHashCode(int syncIndex, int hashCode) { Debug.Assert((0 < syncIndex) && (syncIndex < s_unusedEntryIndex)); // 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 (s_lock.EnterScope()) { int currentHash = s_entries[syncIndex].HashCode; if (currentHash != 0) { return currentHash; } s_entries[syncIndex].HashCode = hashCode; return hashCode; } } /// <summary> /// Sets the hash code assuming the caller holds s_lock. Use for not yet /// published entries only. /// </summary> public static void MoveHashCodeToNewEntry(int syncIndex, int hashCode) { Debug.Assert(s_lock.IsHeldByCurrentThread); Debug.Assert((0 < syncIndex) && (syncIndex < s_unusedEntryIndex)); s_entries[syncIndex].HashCode = hashCode; } /// <summary> /// Initializes the Lock assuming the caller holds s_lock. Use for not yet /// published entries only. /// </summary> public static void MoveThinLockToNewEntry(int syncIndex, int threadId, uint recursionLevel) { Debug.Assert(s_lock.IsHeldByCurrentThread); Debug.Assert((0 < syncIndex) && (syncIndex < s_unusedEntryIndex)); s_entries[syncIndex].Lock.InitializeForMonitor(threadId, recursionLevel); } /// <summary> /// Returns the Monitor synchronization object. The return value is never null. /// </summary> public static Lock GetLockObject(int syncIndex) { // Note that we do not take a lock here. When we replace s_entries, we preserve all // indices and Lock references. return UnsafeEntryRef(syncIndex).Lock; } private sealed class DeadEntryCollector { private int _index; public DeadEntryCollector() { } public void Activate(int index) => _index = index; ~DeadEntryCollector() { if (_index == 0) return; Lock? lockToDispose = default; DependentHandle dependentHandleToDispose = default; using (s_lock.EnterScope()) { ref Entry entry = ref s_entries[_index]; if (entry.Owner.Target != null) { // Retry later if the owner is not collected yet. GC.ReRegisterForFinalize(this); return; } dependentHandleToDispose = entry.Owner; entry.Owner = default; lockToDispose = entry.Lock; entry.Lock = default; entry.Next = s_freeEntryList; s_freeEntryList = _index; } // Dispose outside the lock dependentHandleToDispose.Dispose(); lockToDispose?.Dispose(); } } /// <summary> /// Stores the Monitor synchronization object and the hash code for an arbitrary object. /// </summary> private struct Entry { /// <summary> /// The Monitor synchronization object. /// </summary> public Lock Lock; /// <summary> /// Contains either the hash code or the index of the next freed entry. /// </summary> private int _hashOrNext; /// <summary> /// The dependent GC handle representing the owner object of this sync entry and the collector responsible /// for freeing the entry. /// </summary> public DependentHandle Owner; // Unused field to make the entry even number of words. // we will likely put something in here eventually. internal nint _unusedPadding; /// <summary> /// For entries in use, this property gets or sets the hash code of the owner object. /// The zero value indicates the hash code has not yet been assigned. /// </summary> public int HashCode { get { return _hashOrNext; } set { _hashOrNext = value; } } /// <summary> /// For freed entries, this property gets or sets the index of the next freed entry. /// The zero value indicates the end of the list. /// </summary> public int Next { get { return _hashOrNext; } set { _hashOrNext = value; } } } } }