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