Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.0] Make thread pool thread timeouts configurable #92988

Merged
merged 1 commit into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/coreclr/inc/clrconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -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, "");
Expand Down
28 changes: 28 additions & 0 deletions src/coreclr/vm/comthreadpool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/coreclr/vm/eeconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ HRESULT EEConfig::Init()
bDiagnosticSuspend = false;
#endif

threadPoolThreadTimeoutMs = -2; // not configured
threadPoolThreadsToKeepAlive = 0;

fDisableDefaultCodeVersioning = false;

#if defined(FEATURE_TIERED_COMPILATION)
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/vm/eeconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -680,6 +683,9 @@ class EEConfig
DWORD testThreadAbort;
#endif

int threadPoolThreadTimeoutMs;
int threadPoolThreadsToKeepAlive;

bool fDisableDefaultCodeVersioning;

#if defined(FEATURE_TIERED_COMPILATION)
Expand Down
85 changes: 79 additions & 6 deletions src/coreclr/vm/win32threadpool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -3196,7 +3239,7 @@ DWORD WINAPI ThreadpoolMgr::CompletionPortThreadStart(LPVOID lpArgs)
ThreadCounter::Counts oldCounts;
ThreadCounter::Counts newCounts;

cpThreadWait = CP_THREAD_WAIT;
cpThreadWait = IOCompletionThreadTimeoutMs;
for (;; )
{
Top:
Expand Down Expand Up @@ -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."
//
Expand All @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/coreclr/vm/win32threadpool.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);

//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace System.Threading
{
internal sealed partial class PortableThreadPool
{
private int _numThreadsBeingKeptAlive;

/// <summary>
/// The worker thread infastructure for the CLR thread pool.
/// </summary>
Expand All @@ -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;
}

/// <summary>
/// Semaphore for controlling how many threads are currently working.
/// </summary>
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace System.Threading
/// </summary>
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;
Expand All @@ -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;

Expand Down
Loading