diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 7f6698684f9fb..69b0dd758b751 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -577,6 +577,8 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_ThreadPool_UnfairSemaphoreSpinLimit, W("Thread #else // !TARGET_ARM64 RETAIL_CONFIG_DWORD_INFO(INTERNAL_ThreadPool_UnfairSemaphoreSpinLimit, W("ThreadPool_UnfairSemaphoreSpinLimit"), 0x46, "Maximum number of spins a thread pool worker thread performs before waiting for work") #endif // TARGET_ARM64 +RETAIL_CONFIG_DWORD_INFO_EX(EXTERNAL_ThreadPool_ThreadTimeoutMs, W("ThreadPool_ThreadTimeoutMs"), (DWORD)-2, "The amount of time in milliseconds a thread pool thread waits without having done any work before timing out and exiting. Set to -1 to disable the timeout. Applies to worker threads, completion port threads, and wait threads. Also see the ThreadPool_ThreadsToKeepAlive config value for relevant information.", CLRConfig::LookupOptions::ParseIntegerAsBase10) +RETAIL_CONFIG_DWORD_INFO_EX(EXTERNAL_ThreadPool_ThreadsToKeepAlive, W("ThreadPool_ThreadsToKeepAlive"), 0, "The number of worker or completion port threads to keep alive after they are created. Set to -1 to keep all created worker or completion port threads alive. When the ThreadPool_ThreadTimeoutMs config value is also set, for worker and completion port threads the timeout applies to threads in the respective pool that are in excess of the number configured for ThreadPool_ThreadsToKeepAlive.", CLRConfig::LookupOptions::ParseIntegerAsBase10) RETAIL_CONFIG_DWORD_INFO(INTERNAL_HillClimbing_Disable, W("HillClimbing_Disable"), 0, "Disables hill climbing for thread adjustments in the thread pool"); RETAIL_CONFIG_DWORD_INFO(INTERNAL_HillClimbing_WavePeriod, W("HillClimbing_WavePeriod"), 4, ""); diff --git a/src/coreclr/vm/comthreadpool.cpp b/src/coreclr/vm/comthreadpool.cpp index 578f30e4f3d14..8432da6361157 100644 --- a/src/coreclr/vm/comthreadpool.cpp +++ b/src/coreclr/vm/comthreadpool.cpp @@ -192,6 +192,34 @@ FCIMPL4(INT32, ThreadPoolNative::GetNextConfigUInt32Value, case 19: if (TryGetConfig(CLRConfig::INTERNAL_HillClimbing_SampleIntervalHigh, false, W("System.Threading.ThreadPool.HillClimbing.SampleIntervalHigh"))) { return 20; } FALLTHROUGH; case 20: if (TryGetConfig(CLRConfig::INTERNAL_HillClimbing_GainExponent, false, W("System.Threading.ThreadPool.HillClimbing.GainExponent"))) { return 21; } FALLTHROUGH; + case 21: + { + int threadPoolThreadTimeoutMs = g_pConfig->ThreadPoolThreadTimeoutMs(); + if (threadPoolThreadTimeoutMs >= -1) + { + *configValueRef = (UINT32)threadPoolThreadTimeoutMs; + *isBooleanRef = false; + *appContextConfigNameRef = W("System.Threading.ThreadPool.ThreadTimeoutMs"); + return 22; + } + + FALLTHROUGH; + } + + case 22: + { + int threadPoolThreadsToKeepAlive = g_pConfig->ThreadPoolThreadsToKeepAlive(); + if (threadPoolThreadsToKeepAlive >= -1) + { + *configValueRef = (UINT32)threadPoolThreadsToKeepAlive; + *isBooleanRef = false; + *appContextConfigNameRef = W("System.Threading.ThreadPool.ThreadsToKeepAlive"); + return 23; + } + + FALLTHROUGH; + } + default: *configValueRef = 0; *isBooleanRef = false; diff --git a/src/coreclr/vm/eeconfig.cpp b/src/coreclr/vm/eeconfig.cpp index 01ddbc873b940..f111cf0833263 100644 --- a/src/coreclr/vm/eeconfig.cpp +++ b/src/coreclr/vm/eeconfig.cpp @@ -237,6 +237,9 @@ HRESULT EEConfig::Init() bDiagnosticSuspend = false; #endif + threadPoolThreadTimeoutMs = -2; // not configured + threadPoolThreadsToKeepAlive = 0; + fDisableDefaultCodeVersioning = false; #if defined(FEATURE_TIERED_COMPILATION) @@ -738,6 +741,19 @@ HRESULT EEConfig::sync() #endif //_DEBUG + threadPoolThreadTimeoutMs = + (int)Configuration::GetKnobDWORDValue( + W("System.Threading.ThreadPool.ThreadTimeoutMs"), + CLRConfig::EXTERNAL_ThreadPool_ThreadTimeoutMs); + threadPoolThreadsToKeepAlive = + (int)Configuration::GetKnobDWORDValue( + W("System.Threading.ThreadPool.ThreadsToKeepAlive"), + CLRConfig::EXTERNAL_ThreadPool_ThreadsToKeepAlive); + if (threadPoolThreadsToKeepAlive < -1) + { + threadPoolThreadsToKeepAlive = 0; + } + m_fInteropValidatePinnedObjects = (CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_InteropValidatePinnedObjects) != 0); m_fInteropLogArguments = (CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_InteropLogArguments) != 0); diff --git a/src/coreclr/vm/eeconfig.h b/src/coreclr/vm/eeconfig.h index 8d387ce87e54f..2335752ff92d8 100644 --- a/src/coreclr/vm/eeconfig.h +++ b/src/coreclr/vm/eeconfig.h @@ -483,6 +483,9 @@ class EEConfig #endif + int ThreadPoolThreadTimeoutMs() const { LIMITED_METHOD_CONTRACT; return threadPoolThreadTimeoutMs; } + int ThreadPoolThreadsToKeepAlive() const { LIMITED_METHOD_CONTRACT; return threadPoolThreadsToKeepAlive; } + private: //---------------------------------------------------------------- bool fInited; // have we synced to the registry at least once? @@ -680,6 +683,9 @@ class EEConfig DWORD testThreadAbort; #endif + int threadPoolThreadTimeoutMs; + int threadPoolThreadsToKeepAlive; + bool fDisableDefaultCodeVersioning; #if defined(FEATURE_TIERED_COMPILATION) diff --git a/src/coreclr/vm/win32threadpool.cpp b/src/coreclr/vm/win32threadpool.cpp index 7ffbc34948a47..1c8f26fa75f14 100644 --- a/src/coreclr/vm/win32threadpool.cpp +++ b/src/coreclr/vm/win32threadpool.cpp @@ -126,6 +126,10 @@ SVAL_IMPL(ThreadpoolMgr::LIST_ENTRY,ThreadpoolMgr,TimerQueue); // queue of time unsigned int ThreadpoolMgr::LastCPThreadCreation=0; // last time a completion port thread was created unsigned int ThreadpoolMgr::NumberOfProcessors; // = NumberOfWorkerThreads - no. of blocked threads +DWORD ThreadpoolMgr::WorkerThreadTimeoutMs = 20 * 1000; +DWORD ThreadpoolMgr::IOCompletionThreadTimeoutMs = 15 * 1000; +int ThreadpoolMgr::NumWorkerThreadsBeingKeptAlive = 0; +int ThreadpoolMgr::NumIOCompletionThreadsBeingKeptAlive = 0; CrstStatic ThreadpoolMgr::WorkerCriticalSection; CLREvent * ThreadpoolMgr::RetiredCPWakeupEvent; // wakeup event for completion port threads @@ -354,6 +358,13 @@ BOOL ThreadpoolMgr::Initialize() NumberOfProcessors = GetCurrentProcessCpuCount(); InitPlatformVariables(); + int threadTimeoutMs = g_pConfig->ThreadPoolThreadTimeoutMs(); + if (threadTimeoutMs >= -1) + { + WorkerThreadTimeoutMs = (DWORD)threadTimeoutMs; + IOCompletionThreadTimeoutMs = (DWORD)threadTimeoutMs; + } + EX_TRY { if (!UsePortableThreadPool()) @@ -1809,6 +1820,9 @@ DWORD WINAPI ThreadpoolMgr::WorkerThreadStart(LPVOID lpArgs) ThreadCounter::Counts counts, oldCounts, newCounts; bool foundWork = true, wasNotRecalled = true; + bool isThreadKeepAliveInitialized = false; + bool keepThreadAlive = false; + counts = WorkerCounter.GetCleanCounts(); if (ETW_EVENT_ENABLED(MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_DOTNET_Context, ThreadPoolWorkerThreadStart)) FireEtwThreadPoolWorkerThreadStart(counts.NumActive, counts.NumRetired, GetClrInstanceId()); @@ -1865,6 +1879,35 @@ DWORD WINAPI ThreadpoolMgr::WorkerThreadStart(LPVOID lpArgs) GCX_PREEMP_NO_DTOR(); _ASSERTE(pThread == NULL || !pThread->PreemptiveGCDisabled()); + if (!isThreadKeepAliveInitialized && fThreadInit) + { + // Determine whether to keep this thread alive. Some threads may always be kept alive based on config. + isThreadKeepAliveInitialized = true; + int threadsToKeepAlive = g_pConfig->ThreadPoolThreadsToKeepAlive(); + if (threadsToKeepAlive != 0) + { + if (threadsToKeepAlive < 0) + { + keepThreadAlive = true; + } + else + { + int count = VolatileLoadWithoutBarrier(&NumWorkerThreadsBeingKeptAlive); + while (count < threadsToKeepAlive) + { + int countBeforeUpdate = InterlockedCompareExchangeT(&NumWorkerThreadsBeingKeptAlive, count + 1, count); + if (countBeforeUpdate == count) + { + keepThreadAlive = true; + break; + } + + count = countBeforeUpdate; + } + } + } + } + // make sure there's really work. If not, go back to sleep // counts volatile read paired with CompareExchangeCounts loop set @@ -1951,7 +1994,7 @@ DWORD WINAPI ThreadpoolMgr::WorkerThreadStart(LPVOID lpArgs) while (true) { RetryRetire: - if (RetiredWorkerSemaphore->Wait(WorkerTimeout)) + if (RetiredWorkerSemaphore->Wait(keepThreadAlive ? INFINITE : WorkerThreadTimeoutMs)) { foundWork = true; @@ -2028,7 +2071,7 @@ DWORD WINAPI ThreadpoolMgr::WorkerThreadStart(LPVOID lpArgs) FireEtwThreadPoolWorkerThreadWait(counts.NumActive, counts.NumRetired, GetClrInstanceId()); RetryWaitForWork: - if (WorkerSemaphore->Wait(WorkerTimeout, WorkerThreadSpinLimit, NumberOfProcessors)) + if (WorkerSemaphore->Wait(keepThreadAlive ? INFINITE : WorkerThreadTimeoutMs, WorkerThreadSpinLimit, NumberOfProcessors)) { foundWork = true; goto Work; @@ -3153,13 +3196,13 @@ DWORD WINAPI ThreadpoolMgr::CompletionPortThreadStart(LPVOID lpArgs) PIOCompletionContext context; BOOL fIsCompletionContext; - const DWORD CP_THREAD_WAIT = 15000; /* milliseconds */ - _ASSERTE(GlobalCompletionPort != NULL); BOOL fThreadInit = FALSE; Thread *pThread = NULL; + bool isThreadKeepAliveInitialized = false; + bool keepThreadAlive = false; DWORD cpThreadWait = 0; if (g_fEEStarted) { @@ -3196,7 +3239,7 @@ DWORD WINAPI ThreadpoolMgr::CompletionPortThreadStart(LPVOID lpArgs) ThreadCounter::Counts oldCounts; ThreadCounter::Counts newCounts; - cpThreadWait = CP_THREAD_WAIT; + cpThreadWait = IOCompletionThreadTimeoutMs; for (;; ) { Top: @@ -3224,6 +3267,36 @@ DWORD WINAPI ThreadpoolMgr::CompletionPortThreadStart(LPVOID lpArgs) GCX_PREEMP_NO_DTOR(); + if (!isThreadKeepAliveInitialized && fThreadInit) + { + // Determine whether to keep this thread alive. Some threads may always be kept alive based on config. + isThreadKeepAliveInitialized = true; + int threadsToKeepAlive = g_pConfig->ThreadPoolThreadsToKeepAlive(); + if (threadsToKeepAlive != 0) + { + if (threadsToKeepAlive < 0) + { + keepThreadAlive = true; + } + else + { + int count = VolatileLoadWithoutBarrier(&NumIOCompletionThreadsBeingKeptAlive); + while (count < threadsToKeepAlive) + { + int countBeforeUpdate = + InterlockedCompareExchangeT(&NumIOCompletionThreadsBeingKeptAlive, count + 1, count); + if (countBeforeUpdate == count) + { + keepThreadAlive = true; + break; + } + + count = countBeforeUpdate; + } + } + } + } + // // We're about to wait on the IOCP; mark ourselves as no longer "working." // @@ -3238,7 +3311,7 @@ DWORD WINAPI ThreadpoolMgr::CompletionPortThreadStart(LPVOID lpArgs) // one thread listening for completions. So there's no point in having a timeout; it will // only use power unnecessarily. // - cpThreadWait = (newCounts.NumActive == 1) ? INFINITE : CP_THREAD_WAIT; + cpThreadWait = (newCounts.NumActive == 1 || keepThreadAlive) ? INFINITE : IOCompletionThreadTimeoutMs; if (oldCounts == CPThreadCounter.CompareExchangeCounts(newCounts, oldCounts)) break; diff --git a/src/coreclr/vm/win32threadpool.h b/src/coreclr/vm/win32threadpool.h index 4f3397821ded4..20aeccce7f4e5 100644 --- a/src/coreclr/vm/win32threadpool.h +++ b/src/coreclr/vm/win32threadpool.h @@ -1049,6 +1049,11 @@ class ThreadpoolMgr static unsigned int LastCPThreadCreation; // last time a completion port thread was created static unsigned int NumberOfProcessors; // = NumberOfWorkerThreads - no. of blocked threads + static DWORD WorkerThreadTimeoutMs; + static DWORD IOCompletionThreadTimeoutMs; + static int NumWorkerThreadsBeingKeptAlive; + static int NumIOCompletionThreadsBeingKeptAlive; + static BOOL IsApcPendingOnWaitThread; // Indicates if an APC is pending on the wait thread // This needs to be non-hosted, because worker threads can run prior to EE startup. @@ -1058,8 +1063,6 @@ class ThreadpoolMgr static CrstStatic WorkerCriticalSection; private: - static const DWORD WorkerTimeout = 20 * 1000; - DECLSPEC_ALIGN(MAX_CACHE_LINE_SIZE) SVAL_DECL(ThreadCounter,WorkerCounter); // 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 886e774fd22fb..24f1f03359502 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 @@ -7,6 +7,8 @@ namespace System.Threading { internal sealed partial class PortableThreadPool { + private int _numThreadsBeingKeptAlive; + /// /// The worker thread infastructure for the CLR thread pool. /// @@ -17,6 +19,22 @@ 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 + private static readonly short ThreadsToKeepAlive = DetermineThreadsToKeepAlive(); + + private static short DetermineThreadsToKeepAlive() + { + const short DefaultThreadsToKeepAlive = 0; + + // The number of worker threads to keep alive after they are created. Set to -1 to keep all created worker + // threads alive. When the ThreadTimeoutMs config value is also set, for worker threads the timeout applies to + // worker threads that are in excess of the number configured for ThreadsToKeepAlive. + short threadsToKeepAlive = + AppContextConfigHelper.GetInt16Config( + "System.Threading.ThreadPool.ThreadsToKeepAlive", + DefaultThreadsToKeepAlive); + return threadsToKeepAlive >= -1 ? threadsToKeepAlive : DefaultThreadsToKeepAlive; + } + /// /// Semaphore for controlling how many threads are currently working. /// @@ -51,10 +69,36 @@ private static void WorkerThreadStart() LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; LowLevelLifoSemaphore semaphore = s_semaphore; + // Determine the idle timeout to use for this thread. Some threads may always be kept alive based on config. + int timeoutMs = ThreadPoolThreadTimeoutMs; + if (ThreadsToKeepAlive != 0) + { + if (ThreadsToKeepAlive < 0) + { + timeoutMs = Timeout.Infinite; + } + else + { + int count = threadPoolInstance._numThreadsBeingKeptAlive; + while (count < ThreadsToKeepAlive) + { + int countBeforeUpdate = + Interlocked.CompareExchange(ref threadPoolInstance._numThreadsBeingKeptAlive, count + 1, count); + if (countBeforeUpdate == count) + { + timeoutMs = Timeout.Infinite; + break; + } + + count = countBeforeUpdate; + } + } + } + while (true) { bool spinWait = true; - while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + while (semaphore.Wait(timeoutMs, spinWait)) { bool alreadyRemovedWorkingWorker = false; while (TakeActiveRequest(threadPoolInstance)) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.cs index a425298cacd8e..5dd945b013448 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.cs @@ -12,7 +12,6 @@ namespace System.Threading /// internal sealed partial class PortableThreadPool { - private const int ThreadPoolThreadTimeoutMs = 20 * 1000; // If you change this make sure to change the timeout times in the tests. private const int SmallStackSizeBytes = 256 * 1024; private const short MaxPossibleThreadCount = short.MaxValue; @@ -33,6 +32,22 @@ internal sealed partial class PortableThreadPool private static readonly short ForcedMaxWorkerThreads = AppContextConfigHelper.GetInt16Config("System.Threading.ThreadPool.MaxThreads", 0, false); + private static readonly int ThreadPoolThreadTimeoutMs = DetermineThreadPoolThreadTimeoutMs(); + + private static int DetermineThreadPoolThreadTimeoutMs() + { + const int DefaultThreadPoolThreadTimeoutMs = 20 * 1000; // If you change this make sure to change the timeout times in the tests. + + // The amount of time in milliseconds a thread pool thread waits without having done any work before timing out and + // exiting. Set to -1 to disable the timeout. Applies to worker threads and wait threads. Also see the + // ThreadsToKeepAlive config value for relevant information. + int timeoutMs = + AppContextConfigHelper.GetInt32Config( + "System.Threading.ThreadPool.ThreadTimeoutMs", + DefaultThreadPoolThreadTimeoutMs); + return timeoutMs >= -1 ? timeoutMs : DefaultThreadPoolThreadTimeoutMs; + } + [ThreadStatic] private static object? t_completionCountObject;