diff --git a/src/System.Private.CoreLib/shared/System/Threading/ThreadLocal.cs b/src/System.Private.CoreLib/shared/System/Threading/ThreadLocal.cs index 8c19903ab7c..276caed6213 100644 --- a/src/System.Private.CoreLib/shared/System/Threading/ThreadLocal.cs +++ b/src/System.Private.CoreLib/shared/System/Threading/ThreadLocal.cs @@ -454,16 +454,16 @@ public IList Values /// Gets all of the threads' values in a list. private List? GetValuesAsList() { - List valueList = new List(); + LinkedSlot? linkedSlot = _linkedSlot; int id = ~_idComplement; - if (id == -1) + if (id == -1 || linkedSlot == null) { return null; } // Walk over the linked list of slots and gather the values associated with this ThreadLocal instance. - Debug.Assert(_linkedSlot != null, "Should only be null if the instance was disposed."); - for (LinkedSlot? linkedSlot = _linkedSlot._next; linkedSlot != null; linkedSlot = linkedSlot._next) + var valueList = new List(); + for (linkedSlot = linkedSlot._next; linkedSlot != null; linkedSlot = linkedSlot._next) { // We can safely read linkedSlot.Value. Even if this ThreadLocal has been disposed in the meantime, the LinkedSlot // objects will never be assigned to another ThreadLocal instance. @@ -473,6 +473,32 @@ public IList Values return valueList; } + internal IEnumerable ValuesAsEnumerable + { + get + { + if (!_trackAllValues) + { + throw new InvalidOperationException(SR.ThreadLocal_ValuesNotAvailable); + } + + LinkedSlot? linkedSlot = _linkedSlot; + int id = ~_idComplement; + if (id == -1 || linkedSlot == null) + { + throw new ObjectDisposedException(SR.ThreadLocal_Disposed); + } + + // Walk over the linked list of slots and gather the values associated with this ThreadLocal instance. + for (linkedSlot = linkedSlot._next; linkedSlot != null; linkedSlot = linkedSlot._next) + { + // We can safely read linkedSlot.Value. Even if this ThreadLocal has been disposed in the meantime, the LinkedSlot + // objects will never be assigned to another ThreadLocal instance. + yield return linkedSlot._value; + } + } + } + /// Gets the number of threads that have data in this instance. private int ValuesCountForDebugDisplay { diff --git a/src/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 7158ecdbf83..f2a87d6555f 100644 --- a/src/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -281,6 +281,8 @@ + + diff --git a/src/System.Private.CoreLib/src/System/Threading/ClrThreadPool.cs b/src/System.Private.CoreLib/src/System/Threading/ClrThreadPool.cs index 4252d93e62e..cc03f95972c 100644 --- a/src/System.Private.CoreLib/src/System/Threading/ClrThreadPool.cs +++ b/src/System.Private.CoreLib/src/System/Threading/ClrThreadPool.cs @@ -58,7 +58,7 @@ private struct CacheLineSeparated private CacheLineSeparated _separated; private ulong _currentSampleStartTime; - private int _completionCount = 0; + private readonly ThreadInt64PersistentCounter _completionCounter = new ThreadInt64PersistentCounter(); private int _threadAdjustmentIntervalMs; private LowLevelLock _hillClimbingThreadAdjustmentLock = new LowLevelLock(); @@ -184,7 +184,7 @@ public bool SetMaxThreads(int maxThreads) public int GetAvailableThreads() { ThreadCounts counts = ThreadCounts.VolatileReadCounts(ref _separated.counts); - int count = _maxThreads - counts.numExistingThreads; + int count = _maxThreads - counts.numProcessingWork; if (count < 0) { return 0; @@ -192,10 +192,12 @@ public int GetAvailableThreads() return count; } + public int ThreadCount => ThreadCounts.VolatileReadCounts(ref _separated.counts).numExistingThreads; + public long CompletedWorkItemCount => _completionCounter.Count; + internal bool NotifyWorkItemComplete() { - // TODO: Check perf. Might need to make this thread-local. - Interlocked.Increment(ref _completionCount); + _completionCounter.Increment(); Volatile.Write(ref _separated.lastDequeueTime, Environment.TickCount); if (ShouldAdjustMaxWorkersActive() && _hillClimbingThreadAdjustmentLock.TryAcquire()) @@ -221,7 +223,7 @@ private void AdjustMaxWorkersActive() { _hillClimbingThreadAdjustmentLock.VerifyIsLocked(); int currentTicks = Environment.TickCount; - int totalNumCompletions = Volatile.Read(ref _completionCount); + int totalNumCompletions = (int)_completionCounter.Count; int numCompletions = totalNumCompletions - _separated.priorCompletionCount; ulong startTime = _currentSampleStartTime; ulong endTime = HighPerformanceCounter.TickCount; diff --git a/src/System.Private.CoreLib/src/System/Threading/Lock.cs b/src/System.Private.CoreLib/src/System/Threading/Lock.cs index 1335685a4ec..7739c8a5459 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Lock.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Lock.cs @@ -100,7 +100,7 @@ public bool TryAcquire(TimeSpan timeout) return TryAcquire(WaitHandle.ToTimeoutMilliseconds(timeout)); } - public bool TryAcquire(int millisecondsTimeout) + public bool TryAcquire(int millisecondsTimeout, bool trackContentions = false) { if (millisecondsTimeout < -1) throw new ArgumentOutOfRangeException(nameof(millisecondsTimeout), SR.ArgumentOutOfRange_NeedNonNegOrNegative1); @@ -121,10 +121,10 @@ public bool TryAcquire(int millisecondsTimeout) // // Fall back to the slow path for contention // - return TryAcquireContended(currentThreadId, millisecondsTimeout); + return TryAcquireContended(currentThreadId, millisecondsTimeout, trackContentions); } - private bool TryAcquireContended(IntPtr currentThreadId, int millisecondsTimeout) + private bool TryAcquireContended(IntPtr currentThreadId, int millisecondsTimeout, bool trackContentions = false) { // // If we already own the lock, just increment the recursion count. @@ -185,6 +185,12 @@ private bool TryAcquireContended(IntPtr currentThreadId, int millisecondsTimeout // // Now we wait. // + + if (trackContentions) + { + Monitor.IncrementLockContentionCount(); + } + TimeoutTracker timeoutTracker = TimeoutTracker.Start(millisecondsTimeout); AutoResetEvent ev = Event; diff --git a/src/System.Private.CoreLib/src/System/Threading/Monitor.cs b/src/System.Private.CoreLib/src/System/Threading/Monitor.cs index b9839321a50..72feeabbf6a 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Monitor.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Monitor.cs @@ -184,7 +184,7 @@ internal static bool TryAcquireContended(Lock lck, object obj, int millisecondsT using (new DebugBlockingScope(obj, DebugBlockingItemType.MonitorCriticalSection, millisecondsTimeout, out blockingItem)) { - return lck.TryAcquire(millisecondsTimeout); + return lck.TryAcquire(millisecondsTimeout, trackContentions: true); } } @@ -246,5 +246,19 @@ public void Dispose() } #endregion + + #region Metrics + + private static readonly ThreadInt64PersistentCounter s_lockContentionCounter = new ThreadInt64PersistentCounter(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IncrementLockContentionCount() => s_lockContentionCounter.Increment(); + + /// + /// Gets the number of times there was contention upon trying to take a 's lock so far. + /// + public static long LockContentionCount => s_lockContentionCounter.Count; + + #endregion } } diff --git a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Unix.cs b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Unix.cs index 71151bed6cc..b8479e8dbe3 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Unix.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Unix.cs @@ -142,6 +142,7 @@ internal static void InitializeCom() } public void DisableComObjectEagerCleanup() { } + private static void InitializeExistingThreadPoolThread() { } public void Interrupt() => WaitSubsystem.Interrupt(this); internal static void UninterruptibleSleep0() => WaitSubsystem.UninterruptibleSleep0(); diff --git a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Windows.cs b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Windows.cs index fd506466bc6..89de823f021 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Windows.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.Windows.cs @@ -4,6 +4,7 @@ using Microsoft.Win32.SafeHandles; using System.Diagnostics; +using System.Runtime; using System.Runtime.InteropServices; namespace System.Threading @@ -331,6 +332,13 @@ private static void UninitializeCom() // TODO: https://github.com/dotnet/corefx/issues/20766 public void DisableComObjectEagerCleanup() { } + + private static void InitializeExistingThreadPoolThread() + { + InitializeCom(); + ThreadPool.InitializeForThreadPoolThread(); + } + public void Interrupt() { throw new PlatformNotSupportedException(); } internal static void UninterruptibleSleep0() diff --git a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.cs b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.cs index 075f206184b..29135667525 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Thread.CoreRT.cs @@ -93,7 +93,7 @@ private static Thread InitializeExistingThread(bool threadPoolThread) if (threadPoolThread) { - InitializeCom(); + InitializeExistingThreadPoolThread(); } return currentThread; diff --git a/src/System.Private.CoreLib/src/System/Threading/ThreadBooleanCounter.cs b/src/System.Private.CoreLib/src/System/Threading/ThreadBooleanCounter.cs new file mode 100644 index 00000000000..ee77a4efd1d --- /dev/null +++ b/src/System.Private.CoreLib/src/System/Threading/ThreadBooleanCounter.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Threading +{ + internal sealed class ThreadBooleanCounter + { + private readonly ThreadLocal _threadLocalFlag = new ThreadLocal(trackAllValues: true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set() + { + Debug.Assert(!_threadLocalFlag.Value); + + try + { + _threadLocalFlag.Value = true; + } + catch (OutOfMemoryException) + { + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + Debug.Assert(!_threadLocalFlag.IsValueCreated || _threadLocalFlag.Value); + _threadLocalFlag.Value = false; + } + + public int Count + { + get + { + int count = 0; + try + { + foreach (bool isSet in _threadLocalFlag.ValuesAsEnumerable) + { + if (isSet) + { + ++count; + Debug.Assert(count > 0); + } + } + return count; + } + catch (OutOfMemoryException) + { + // Some allocation occurs above and it may be a bit awkward to get an OOM from this property getter + return count; + } + } + } + } +} diff --git a/src/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs b/src/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs new file mode 100644 index 00000000000..e5d9cd39a51 --- /dev/null +++ b/src/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Threading +{ + internal sealed class ThreadInt64PersistentCounter + { + // This type is used by Monitor for lock contention counting, so can't use an object for a lock. Also it's preferable + // (though currently not required) to disallow/ignore thread interrupt for uses of this lock here. Using Lock directly + // is a possibility but maybe less compatible with other runtimes. Lock cases are relatively rare, static instance + // should be ok. + private static readonly LowLevelLock s_lock = new LowLevelLock(); + + private readonly ThreadLocal _threadLocalNode = new ThreadLocal(trackAllValues: true); + private long _overflowCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Increment() + { + ThreadLocalNode node = _threadLocalNode.Value; + if (node != null) + { + node.Increment(); + return; + } + + TryCreateNode(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void TryCreateNode() + { + Debug.Assert(_threadLocalNode.Value == null); + + try + { + _threadLocalNode.Value = new ThreadLocalNode(this); + } + catch (OutOfMemoryException) + { + } + } + + public long Count + { + get + { + long count = 0; + try + { + s_lock.Acquire(); + try + { + count = _overflowCount; + foreach (ThreadLocalNode node in _threadLocalNode.ValuesAsEnumerable) + { + count += node.Count; + } + return count; + } + finally + { + s_lock.Release(); + } + } + catch (OutOfMemoryException) + { + // Some allocation occurs above and it may be a bit awkward to get an OOM from this property getter + return count; + } + } + } + + private sealed class ThreadLocalNode + { + private uint _count; + private readonly ThreadInt64PersistentCounter _counter; + + public ThreadLocalNode(ThreadInt64PersistentCounter counter) + { + Debug.Assert(counter != null); + + _count = 1; + _counter = counter; + } + + ~ThreadLocalNode() + { + ThreadInt64PersistentCounter counter = _counter; + s_lock.Acquire(); + try + { + counter._overflowCount += _count; + } + finally + { + s_lock.Release(); + } + } + + public uint Count => _count; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Increment() + { + uint newCount = _count + 1; + if (newCount != 0) + { + _count = newCount; + return; + } + + OnIncrementOverflow(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnIncrementOverflow() + { + // Accumulate the count for this increment into the overflow count and reset the thread-local count + + // The lock, in coordination with other places that read these values, ensures that both changes below become + // visible together + ThreadInt64PersistentCounter counter = _counter; + s_lock.Acquire(); + try + { + _count = 0; + counter._overflowCount += (long)uint.MaxValue + 1; + } + finally + { + s_lock.Release(); + } + } + } + } +} diff --git a/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs b/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs index e59d5df2da1..a0045b92e5e 100644 --- a/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs +++ b/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs @@ -369,6 +369,22 @@ public static void GetAvailableThreads(out int workerThreads, out int completion completionPortThreads = 0; } + /// + /// Gets the number of thread pool threads that currently exist. + /// + /// + /// For a thread pool implementation that may have different types of threads, the count includes all types. + /// + public static int ThreadCount => ClrThreadPool.ThreadPoolInstance.ThreadCount; + + /// + /// Gets the number of work items that have been processed by the thread pool so far. + /// + /// + /// For a thread pool implementation that may have different types of work items, the count includes all types. + /// + public static long CompletedWorkItemCount => ClrThreadPool.ThreadPoolInstance.CompletedWorkItemCount; + /// /// This method is called to request a new thread pool worker to handle pending work. /// diff --git a/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs b/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs index 748ba677e0a..bfb075a96f1 100644 --- a/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs +++ b/src/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs @@ -4,6 +4,8 @@ using Microsoft.Win32.SafeHandles; using System.Diagnostics; +using System.Runtime; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Threading @@ -62,6 +64,7 @@ internal static void RegisteredWaitCallback(IntPtr instance, IntPtr context, Int bool timedOut = (waitResult == (uint)Interop.Kernel32.WAIT_TIMEOUT); registeredWaitHandle.PerformCallback(timedOut); + ThreadPool.IncrementCompletedWorkItemCount(); wrapper.Exit(); } @@ -244,8 +247,17 @@ public static partial class ThreadPool private static IntPtr s_work; + private static readonly ThreadBooleanCounter s_threadCounter = new ThreadBooleanCounter(); + // The number of threads executing work items in the Dispatch method - private static volatile int numWorkingThreads; + private static readonly ThreadBooleanCounter s_workingThreadCounter = new ThreadBooleanCounter(); + + private static readonly ThreadInt64PersistentCounter s_completedWorkItemCounter = new ThreadInt64PersistentCounter(); + + internal static void InitializeForThreadPoolThread() => s_threadCounter.Set(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IncrementCompletedWorkItemCount() => s_completedWorkItemCounter.Increment(); public static bool SetMaxThreads(int workerThreads, int completionPortThreads) { @@ -276,12 +288,28 @@ public static void GetMinThreads(out int workerThreads, out int completionPortTh public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads) { // Make sure we return a non-negative value if thread pool defaults are changed - int availableThreads = Math.Max(MaxThreadCount - numWorkingThreads, 0); + int availableThreads = Math.Max(MaxThreadCount - s_workingThreadCounter.Count, 0); workerThreads = availableThreads; completionPortThreads = availableThreads; } + /// + /// Gets the number of thread pool threads that currently exist. + /// + /// + /// For a thread pool implementation that may have different types of threads, the count includes all types. + /// + public static int ThreadCount => s_threadCounter.Count; + + /// + /// Gets the number of work items that have been processed so far. + /// + /// + /// For a thread pool implementation that may have different types of work items, the count includes all types. + /// + public static long CompletedWorkItemCount => s_completedWorkItemCounter.Count; + internal static bool KeepDispatching(int startTickCount) { // Note: this function may incorrectly return false due to TickCount overflow @@ -290,12 +318,13 @@ internal static bool KeepDispatching(int startTickCount) return ((uint)(Environment.TickCount - startTickCount) < DispatchQuantum); } - internal static void NotifyWorkItemProgress() - { - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotifyWorkItemProgress() => IncrementCompletedWorkItemCount(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool NotifyWorkItemComplete() { + IncrementCompletedWorkItemCount(); return true; } @@ -304,10 +333,10 @@ private static void DispatchCallback(IntPtr instance, IntPtr context, IntPtr wor { var wrapper = ThreadPoolCallbackWrapper.Enter(); Debug.Assert(s_work == work); - Interlocked.Increment(ref numWorkingThreads); + ThreadBooleanCounter workingThreadCounter = s_workingThreadCounter; + workingThreadCounter.Set(); ThreadPoolWorkQueue.Dispatch(); - int numWorkers = Interlocked.Decrement(ref numWorkingThreads); - Debug.Assert(numWorkers >= 0); + workingThreadCounter.Clear(); // We reset the thread after executing each callback wrapper.Exit(resetThread: false); } diff --git a/src/System.Private.CoreLib/src/System/Threading/Timer.Windows.cs b/src/System.Private.CoreLib/src/System/Threading/Timer.Windows.cs index 3c4ac053c9c..ae4b6d78fc6 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Timer.Windows.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Timer.Windows.cs @@ -27,6 +27,7 @@ private static void TimerCallback(IntPtr instance, IntPtr context, IntPtr timer) int id = (int)context; var wrapper = ThreadPoolCallbackWrapper.Enter(); Instances[id].FireNextTimers(); + ThreadPool.IncrementCompletedWorkItemCount(); wrapper.Exit(); } diff --git a/src/System.Private.CoreLib/src/System/Threading/Win32ThreadPoolBoundHandle.cs b/src/System.Private.CoreLib/src/System/Threading/Win32ThreadPoolBoundHandle.cs index c9ea8e62e19..e08bd7c110a 100644 --- a/src/System.Private.CoreLib/src/System/Threading/Win32ThreadPoolBoundHandle.cs +++ b/src/System.Private.CoreLib/src/System/Threading/Win32ThreadPoolBoundHandle.cs @@ -173,6 +173,7 @@ private static unsafe void OnNativeIOCompleted(IntPtr instance, IntPtr context, boundHandle.Release(); Win32ThreadPoolNativeOverlapped.CompleteWithCallback(ioResult, (uint)numberOfBytesTransferred, overlapped); + ThreadPool.IncrementCompletedWorkItemCount(); wrapper.Exit(); }