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 ca9f5435d6a1f..15fd70ea73ace 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 @@ -2539,6 +2539,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs new file mode 100644 index 0000000000000..2e634fd469d9d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @@ -0,0 +1,81 @@ +// 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.Tracing; + +namespace System.Threading +{ + internal sealed partial class PortableThreadPool + { + /// + /// The worker thread infastructure for the CLR thread pool. + /// + private static partial class WorkerThread + { + + /// + /// Semaphore for controlling how many threads are currently working. + /// + private static readonly LowLevelLifoSemaphore s_semaphore = + new LowLevelLifoSemaphore( + 0, + MaxPossibleThreadCount, + AppContextConfigHelper.GetInt32Config( + "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", + SemaphoreSpinCountDefault, + false), + onWait: () => + { + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( + (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + }); + + private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + + private static void WorkerThreadStart() + { + Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + + PortableThreadPool threadPoolInstance = ThreadPoolInstance; + + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( + (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + + LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; + LowLevelLifoSemaphore semaphore = s_semaphore; + + while (true) + { + bool spinWait = true; + while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + { + WorkerDoWork(threadPoolInstance, ref spinWait); + } + + if (ShouldExitWorker(threadPoolInstance, threadAdjustmentLock)) + { + break; + } + } + } + + + private static void CreateWorkerThread() + { + // Thread pool threads must start in the default execution context without transferring the context, so + // using UnsafeStart() instead of Start() + Thread workerThread = new Thread(s_workerThreadStart); + workerThread.IsThreadPoolThread = true; + workerThread.IsBackground = true; + // thread name will be set in thread proc + workerThread.UnsafeStart(); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs index 96578b9de6b8d..b8aac3a896b11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; namespace System.Threading { @@ -28,147 +29,112 @@ private static partial class WorkerThread // preexisting threads from running out of memory when using new stack space in low-memory situations. public const int EstimatedAdditionalStackUsagePerThreadBytes = 64 << 10; // 64 KB - /// - /// Semaphore for controlling how many threads are currently working. - /// - private static readonly LowLevelLifoSemaphore s_semaphore = - new LowLevelLifoSemaphore( - 0, - MaxPossibleThreadCount, - AppContextConfigHelper.GetInt32Config( - "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", - SemaphoreSpinCountDefault, - false), - onWait: () => + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait) + { + bool alreadyRemovedWorkingWorker = false; + while (TakeActiveRequest(threadPoolInstance)) + { + threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; + if (!ThreadPoolWorkQueue.Dispatch()) { - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( - (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); - } - }); + // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have + // already removed this working worker in the counts. This typically happens when hill climbing + // decreases the worker thread count goal. + alreadyRemovedWorkingWorker = true; + break; + } - private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + if (threadPoolInstance._separated.numRequestedWorkers <= 0) + { + break; + } - private static void WorkerThreadStart() - { - Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + // In highly bursty cases with short bursts of work, especially in the portable thread pool + // implementation, worker threads are being released and entering Dispatch very quickly, not finding + // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on + // data and some interlocked operations, and similarly when the thread pool runs out of work. Since + // there is a pending request for work, introduce a slight delay before serving the next request. + // The spin-wait is mainly for when the sleep is not effective due to there being no other threads + // to schedule. + Thread.UninterruptibleSleep0(); + if (!Environment.IsSingleProcessor) + { + Thread.SpinWait(1); + } + } - PortableThreadPool threadPoolInstance = ThreadPoolInstance; + // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, + // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that + // the semaphore would be released within the spin-wait window + spinWait = !alreadyRemovedWorkingWorker; - if (NativeRuntimeEventSource.Log.IsEnabled()) + if (!alreadyRemovedWorkingWorker) { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( - (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + // If we woke up but couldn't find a request, or ran out of work items to process, we need to update + // the number of working workers to reflect that we are done working for now + RemoveWorkingWorker(threadPoolInstance); } + } - LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; - LowLevelLifoSemaphore semaphore = s_semaphore; + // returns true if the worker is shutting down + // returns false if we should do another iteration + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ShouldExitWorker (PortableThreadPool threadPoolInstance, LowLevelLock threadAdjustmentLock) + { + // The thread cannot exit if it has IO pending, otherwise the IO may be canceled + if (IsIOPending) + { + return false; + } - while (true) + threadAdjustmentLock.Acquire(); + try { - bool spinWait = true; - while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + // At this point, the thread's wait timed out. We are shutting down this thread. + // We are going to decrement the number of existing threads to no longer include this one + // and then change the max number of threads in the thread pool to reflect that we don't need as many + // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) { - bool alreadyRemovedWorkingWorker = false; - while (TakeActiveRequest(threadPoolInstance)) - { - threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; - if (!ThreadPoolWorkQueue.Dispatch()) - { - // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have - // already removed this working worker in the counts. This typically happens when hill climbing - // decreases the worker thread count goal. - alreadyRemovedWorkingWorker = true; - break; - } - - if (threadPoolInstance._separated.numRequestedWorkers <= 0) - { - break; - } - - // In highly bursty cases with short bursts of work, especially in the portable thread pool - // implementation, worker threads are being released and entering Dispatch very quickly, not finding - // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on - // data and some interlocked operations, and similarly when the thread pool runs out of work. Since - // there is a pending request for work, introduce a slight delay before serving the next request. - // The spin-wait is mainly for when the sleep is not effective due to there being no other threads - // to schedule. - Thread.UninterruptibleSleep0(); - if (!Environment.IsSingleProcessor) - { - Thread.SpinWait(1); - } - } - - // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, - // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that - // the semaphore would be released within the spin-wait window - spinWait = !alreadyRemovedWorkingWorker; - - if (!alreadyRemovedWorkingWorker) + // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, + // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not + // decreased below NumProcessingWork, as that would be indicative of such a case. + if (counts.NumExistingThreads <= counts.NumProcessingWork) { - // If we woke up but couldn't find a request, or ran out of work items to process, we need to update - // the number of working workers to reflect that we are done working for now - RemoveWorkingWorker(threadPoolInstance); + // In this case, enough work came in that this thread should not time out and should go back to work. + return false; } - } - - // The thread cannot exit if it has IO pending, otherwise the IO may be canceled - if (IsIOPending) - { - continue; - } - threadAdjustmentLock.Acquire(); - try - { - // At this point, the thread's wait timed out. We are shutting down this thread. - // We are going to decrement the number of existing threads to no longer include this one - // and then change the max number of threads in the thread pool to reflect that we don't need as many - // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) + ThreadCounts newCounts = counts; + short newNumExistingThreads = --newCounts.NumExistingThreads; + short newNumThreadsGoal = + Math.Max( + threadPoolInstance.MinThreadsGoal, + Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); + newCounts.NumThreadsGoal = newNumThreadsGoal; + + ThreadCounts oldCounts = + threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + if (oldCounts == counts) { - // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, - // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not - // decreased below NumProcessingWork, as that would be indicative of such a case. - if (counts.NumExistingThreads <= counts.NumProcessingWork) + HillClimbing.ThreadPoolHillClimber.ForceChange( + newNumThreadsGoal, + HillClimbing.StateOrTransition.ThreadTimedOut); + if (NativeRuntimeEventSource.Log.IsEnabled()) { - // In this case, enough work came in that this thread should not time out and should go back to work. - break; + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); } - - ThreadCounts newCounts = counts; - short newNumExistingThreads = --newCounts.NumExistingThreads; - short newNumThreadsGoal = - Math.Max( - threadPoolInstance.MinThreadsGoal, - Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); - newCounts.NumThreadsGoal = newNumThreadsGoal; - - ThreadCounts oldCounts = - threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - if (oldCounts == counts) - { - HillClimbing.ThreadPoolHillClimber.ForceChange( - newNumThreadsGoal, - HillClimbing.StateOrTransition.ThreadTimedOut); - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); - } - return; - } - - counts = oldCounts; + return true; } + + counts = oldCounts; } - finally - { - threadAdjustmentLock.Release(); - } + } + finally + { + threadAdjustmentLock.Release(); } } @@ -300,17 +266,6 @@ private static bool TakeActiveRequest(PortableThreadPool threadPoolInstance) } return false; } - - private static void CreateWorkerThread() - { - // Thread pool threads must start in the default execution context without transferring the context, so - // using UnsafeStart() instead of Start() - Thread workerThread = new Thread(s_workerThreadStart); - workerThread.IsThreadPoolThread = true; - workerThread.IsBackground = true; - // thread name will be set in thread proc - workerThread.UnsafeStart(); - } } } }