1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Runtime.InteropServices; 6 7 namespace System.Threading 8 { 9 /// <summary> 10 /// A thread-pool run and managed on the CLR. 11 /// </summary> 12 internal partial class ClrThreadPool 13 { 14 #pragma warning disable IDE1006 // Naming Styles 15 public static readonly ClrThreadPool ThreadPoolInstance = new ClrThreadPool(); 16 #pragma warning restore IDE1006 // Naming Styles 17 18 private const int ThreadPoolThreadTimeoutMs = 20 * 1000; // If you change this make sure to change the timeout times in the tests. 19 20 private const short MaxPossibleThreadCount = short.MaxValue; 21 22 private const int CpuUtilizationHigh = 95; 23 private const int CpuUtilizationLow = 80; 24 private int _cpuUtilization = 0; 25 26 27 private static readonly short s_forcedMinWorkerThreads = AppContextConfigHelper.GetInt16Config("System.Threading.ThreadPool.MinThreads", 0); 28 private static readonly short s_forcedMaxWorkerThreads = AppContextConfigHelper.GetInt16Config("System.Threading.ThreadPool.MaxThreads", 0); 29 30 private short _minThreads = (short)ThreadPoolGlobals.processorCount; 31 private short _maxThreads = MaxPossibleThreadCount; 32 private readonly LowLevelLock _maxMinThreadLock = new LowLevelLock(); 33 34 [StructLayout(LayoutKind.Explicit, Size = CacheLineSize * 5)] 35 private struct CacheLineSeparated 36 { 37 #if ARM64 38 private const int CacheLineSize = 128; 39 #else 40 private const int CacheLineSize = 64; 41 #endif 42 [FieldOffset(CacheLineSize * 1)] 43 public ThreadCounts counts; 44 [FieldOffset(CacheLineSize * 2)] 45 public int lastDequeueTime; 46 [FieldOffset(CacheLineSize * 3)] 47 public int priorCompletionCount; 48 [FieldOffset(CacheLineSize * 3 + sizeof(int))] 49 public int priorCompletedWorkRequestsTime; 50 [FieldOffset(CacheLineSize * 3 + sizeof(int) * 2)] 51 public int nextCompletedWorkRequestsTime; 52 } 53 54 private CacheLineSeparated _separated; 55 private ulong _currentSampleStartTime; 56 private int _completionCount = 0; 57 private int _threadAdjustmentIntervalMs; 58 59 private LowLevelLock _hillClimbingThreadAdjustmentLock = new LowLevelLock(); 60 61 private volatile int _numRequestedWorkers = 0; 62 ClrThreadPool()63 private ClrThreadPool() 64 { 65 _separated = new CacheLineSeparated 66 { 67 counts = new ThreadCounts 68 { 69 numThreadsGoal = s_forcedMinWorkerThreads > 0 ? s_forcedMinWorkerThreads : _minThreads 70 } 71 }; 72 } 73 SetMinThreads(int minThreads)74 public bool SetMinThreads(int minThreads) 75 { 76 _maxMinThreadLock.Acquire(); 77 try 78 { 79 if (minThreads < 0 || minThreads > _maxThreads) 80 { 81 return false; 82 } 83 else 84 { 85 short threads = (short)Math.Min(minThreads, MaxPossibleThreadCount); 86 if (s_forcedMinWorkerThreads == 0) 87 { 88 _minThreads = threads; 89 90 ThreadCounts counts = ThreadCounts.VolatileReadCounts(ref _separated.counts); 91 while (counts.numThreadsGoal < _minThreads) 92 { 93 ThreadCounts newCounts = counts; 94 newCounts.numThreadsGoal = _minThreads; 95 96 ThreadCounts oldCounts = ThreadCounts.CompareExchangeCounts(ref _separated.counts, newCounts, counts); 97 if (oldCounts == counts) 98 { 99 counts = newCounts; 100 101 if (newCounts.numThreadsGoal > oldCounts.numThreadsGoal && _numRequestedWorkers > 0) 102 { 103 WorkerThread.MaybeAddWorkingWorker(); 104 } 105 } 106 else 107 { 108 counts = oldCounts; 109 } 110 } 111 } 112 return true; 113 } 114 } 115 finally 116 { 117 _maxMinThreadLock.Release(); 118 } 119 } 120 GetMinThreads()121 public int GetMinThreads() => _minThreads; 122 SetMaxThreads(int maxThreads)123 public bool SetMaxThreads(int maxThreads) 124 { 125 _maxMinThreadLock.Acquire(); 126 try 127 { 128 if (maxThreads < _minThreads || maxThreads == 0) 129 { 130 return false; 131 } 132 else 133 { 134 short threads = (short)Math.Min(maxThreads, MaxPossibleThreadCount); 135 if (s_forcedMaxWorkerThreads == 0) 136 { 137 _maxThreads = threads; 138 139 ThreadCounts counts = ThreadCounts.VolatileReadCounts(ref _separated.counts); 140 while (counts.numThreadsGoal > _maxThreads) 141 { 142 ThreadCounts newCounts = counts; 143 newCounts.numThreadsGoal = _maxThreads; 144 145 ThreadCounts oldCounts = ThreadCounts.CompareExchangeCounts(ref _separated.counts, newCounts, counts); 146 if (oldCounts == counts) 147 { 148 counts = newCounts; 149 } 150 else 151 { 152 counts = oldCounts; 153 } 154 } 155 } 156 return true; 157 } 158 } 159 finally 160 { 161 _maxMinThreadLock.Release(); 162 } 163 } 164 GetMaxThreads()165 public int GetMaxThreads() => _maxThreads; 166 GetAvailableThreads()167 public int GetAvailableThreads() 168 { 169 ThreadCounts counts = ThreadCounts.VolatileReadCounts(ref _separated.counts); 170 int count = _maxThreads - counts.numExistingThreads; 171 if (count < 0) 172 { 173 return 0; 174 } 175 return count; 176 } 177 NotifyWorkItemComplete()178 internal bool NotifyWorkItemComplete() 179 { 180 // TODO: Check perf. Might need to make this thread-local. 181 Interlocked.Increment(ref _completionCount); 182 Volatile.Write(ref _separated.lastDequeueTime, Environment.TickCount); 183 184 if (ShouldAdjustMaxWorkersActive()) 185 { 186 bool acquiredLock = _hillClimbingThreadAdjustmentLock.TryAcquire(); 187 try 188 { 189 if (acquiredLock) 190 { 191 AdjustMaxWorkersActive(); 192 } 193 } 194 finally 195 { 196 if (acquiredLock) 197 { 198 _hillClimbingThreadAdjustmentLock.Release(); 199 } 200 } 201 } 202 203 return !WorkerThread.ShouldStopProcessingWorkNow(); 204 } 205 206 // 207 // This method must only be called if ShouldAdjustMaxWorkersActive has returned true, *and* 208 // _hillClimbingThreadAdjustmentLock is held. 209 // AdjustMaxWorkersActive()210 private void AdjustMaxWorkersActive() 211 { 212 _hillClimbingThreadAdjustmentLock.VerifyIsLocked(); 213 int currentTicks = Environment.TickCount; 214 int totalNumCompletions = Volatile.Read(ref _completionCount); 215 int numCompletions = totalNumCompletions - _separated.priorCompletionCount; 216 ulong startTime = _currentSampleStartTime; 217 ulong endTime = HighPerformanceCounter.TickCount; 218 ulong freq = HighPerformanceCounter.Frequency; 219 220 double elapsedSeconds = (double)(endTime - startTime) / freq; 221 222 if(elapsedSeconds * 1000 >= _threadAdjustmentIntervalMs / 2) 223 { 224 ThreadCounts currentCounts = ThreadCounts.VolatileReadCounts(ref _separated.counts); 225 int newMax; 226 (newMax, _threadAdjustmentIntervalMs) = HillClimbing.ThreadPoolHillClimber.Update(currentCounts.numThreadsGoal, elapsedSeconds, numCompletions); 227 228 while(newMax != currentCounts.numThreadsGoal) 229 { 230 ThreadCounts newCounts = currentCounts; 231 newCounts.numThreadsGoal = (short)newMax; 232 233 ThreadCounts oldCounts = ThreadCounts.CompareExchangeCounts(ref _separated.counts, newCounts, currentCounts); 234 if (oldCounts == currentCounts) 235 { 236 // 237 // If we're increasing the max, inject a thread. If that thread finds work, it will inject 238 // another thread, etc., until nobody finds work or we reach the new maximum. 239 // 240 // If we're reducing the max, whichever threads notice this first will sleep and timeout themselves. 241 // 242 if (newMax > oldCounts.numThreadsGoal) 243 { 244 WorkerThread.MaybeAddWorkingWorker(); 245 } 246 break; 247 } 248 else 249 { 250 if(oldCounts.numThreadsGoal > currentCounts.numThreadsGoal && oldCounts.numThreadsGoal >= newMax) 251 { 252 // someone (probably the gate thread) increased the thread count more than 253 // we are about to do. Don't interfere. 254 break; 255 } 256 257 currentCounts = oldCounts; 258 } 259 } 260 _separated.priorCompletionCount = totalNumCompletions; 261 _separated.nextCompletedWorkRequestsTime = currentTicks + _threadAdjustmentIntervalMs; 262 Volatile.Write(ref _separated.priorCompletedWorkRequestsTime, currentTicks); 263 _currentSampleStartTime = endTime; 264 } 265 } 266 ShouldAdjustMaxWorkersActive()267 private bool ShouldAdjustMaxWorkersActive() 268 { 269 // We need to subtract by prior time because Environment.TickCount can wrap around, making a comparison of absolute times unreliable. 270 int priorTime = Volatile.Read(ref _separated.priorCompletedWorkRequestsTime); 271 int requiredInterval = _separated.nextCompletedWorkRequestsTime - priorTime; 272 int elapsedInterval = Environment.TickCount - priorTime; 273 if(elapsedInterval >= requiredInterval) 274 { 275 ThreadCounts counts = ThreadCounts.VolatileReadCounts(ref _separated.counts); 276 return counts.numExistingThreads >= counts.numThreadsGoal; 277 } 278 return false; 279 } 280 RequestWorker()281 internal void RequestWorker() 282 { 283 Interlocked.Increment(ref _numRequestedWorkers); 284 WorkerThread.MaybeAddWorkingWorker(); 285 GateThread.EnsureRunning(); 286 } 287 } 288 } 289