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.Collections; 6 using System.Collections.Generic; 7 using System.Runtime.InteropServices; 8 using System.Threading; 9 10 namespace System.Net 11 { 12 /// <summary> 13 /// <para>Acts as countdown timer, used to measure elapsed time over a sync operation.</para> 14 /// </summary> 15 internal static class TimerThread 16 { 17 /// <summary> 18 /// <para>Represents a queue of timers, which all have the same duration.</para> 19 /// </summary> 20 internal abstract class Queue 21 { 22 private readonly int _durationMilliseconds; 23 Queue(int durationMilliseconds)24 internal Queue(int durationMilliseconds) 25 { 26 _durationMilliseconds = durationMilliseconds; 27 } 28 29 /// <summary> 30 /// <para>The duration in milliseconds of timers in this queue.</para> 31 /// </summary> 32 internal int Duration => _durationMilliseconds; 33 34 /// <summary> 35 /// <para>Creates and returns a handle to a new timer with attached context.</para> 36 /// </summary> CreateTimer(Callback callback, object context)37 internal abstract Timer CreateTimer(Callback callback, object context); 38 } 39 40 /// <summary> 41 /// <para>Represents a timer and provides a mechanism to cancel.</para> 42 /// </summary> 43 internal abstract class Timer : IDisposable 44 { 45 private readonly int _startTimeMilliseconds; 46 private readonly int _durationMilliseconds; 47 Timer(int durationMilliseconds)48 internal Timer(int durationMilliseconds) 49 { 50 _durationMilliseconds = durationMilliseconds; 51 _startTimeMilliseconds = Environment.TickCount; 52 } 53 54 /// <summary> 55 /// <para>The time (relative to Environment.TickCount) when the timer started.</para> 56 /// </summary> 57 internal int StartTime => _startTimeMilliseconds; 58 59 /// <summary> 60 /// <para>The time (relative to Environment.TickCount) when the timer will expire.</para> 61 /// </summary> 62 internal int Expiration => unchecked(_startTimeMilliseconds + _durationMilliseconds); 63 64 /// <summary> 65 /// <para>Cancels the timer. Returns true if the timer hasn't and won't fire; false if it has or will.</para> 66 /// </summary> Cancel()67 internal abstract bool Cancel(); 68 69 /// <summary> 70 /// <para>Whether or not the timer has expired.</para> 71 /// </summary> 72 internal abstract bool HasExpired { get; } 73 Dispose()74 public void Dispose() => Cancel(); 75 } 76 77 /// <summary> 78 /// <para>Prototype for the callback that is called when a timer expires.</para> 79 /// </summary> Callback(Timer timer, int timeNoticed, object context)80 internal delegate void Callback(Timer timer, int timeNoticed, object context); 81 82 private const int ThreadIdleTimeoutMilliseconds = 30 * 1000; 83 private const int CacheScanPerIterations = 32; 84 private const int TickCountResolution = 15; 85 86 private static readonly LinkedList<WeakReference> s_queues = new LinkedList<WeakReference>(); 87 private static readonly LinkedList<WeakReference> s_newQueues = new LinkedList<WeakReference>(); 88 private static int s_threadState = (int)TimerThreadState.Idle; // Really a TimerThreadState, but need an int for Interlocked. 89 private static readonly AutoResetEvent s_threadReadyEvent = new AutoResetEvent(false); 90 private static readonly ManualResetEvent s_threadShutdownEvent = new ManualResetEvent(false); 91 private static readonly WaitHandle[] s_threadEvents = { s_threadShutdownEvent, s_threadReadyEvent }; 92 private static int s_cacheScanIteration; 93 private static readonly Hashtable s_queuesCache = new Hashtable(); 94 95 /// <summary> 96 /// <para>The possible states of the timer thread.</para> 97 /// </summary> 98 private enum TimerThreadState 99 { 100 Idle, 101 Running, 102 Stopped 103 } 104 105 /// <summary> 106 /// <para>Queue factory. Always synchronized.</para> 107 /// </summary> GetOrCreateQueue(int durationMilliseconds)108 internal static Queue GetOrCreateQueue(int durationMilliseconds) 109 { 110 if (durationMilliseconds == Timeout.Infinite) 111 { 112 return new InfiniteTimerQueue(); 113 } 114 115 if (durationMilliseconds < 0) 116 { 117 throw new ArgumentOutOfRangeException(nameof(durationMilliseconds)); 118 } 119 120 TimerQueue queue; 121 object key = durationMilliseconds; // Box once. 122 WeakReference weakQueue = (WeakReference)s_queuesCache[key]; 123 if (weakQueue == null || (queue = (TimerQueue)weakQueue.Target) == null) 124 { 125 lock (s_newQueues) 126 { 127 weakQueue = (WeakReference)s_queuesCache[key]; 128 if (weakQueue == null || (queue = (TimerQueue)weakQueue.Target) == null) 129 { 130 queue = new TimerQueue(durationMilliseconds); 131 weakQueue = new WeakReference(queue); 132 s_newQueues.AddLast(weakQueue); 133 s_queuesCache[key] = weakQueue; 134 135 // Take advantage of this lock to periodically scan the table for garbage. 136 if (++s_cacheScanIteration % CacheScanPerIterations == 0) 137 { 138 var garbage = new List<object>(); 139 // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations. 140 IDictionaryEnumerator e = s_queuesCache.GetEnumerator(); 141 while (e.MoveNext()) 142 { 143 DictionaryEntry pair = e.Entry; 144 if (((WeakReference)pair.Value).Target == null) 145 { 146 garbage.Add(pair.Key); 147 } 148 } 149 for (int i = 0; i < garbage.Count; i++) 150 { 151 s_queuesCache.Remove(garbage[i]); 152 } 153 } 154 } 155 } 156 } 157 158 return queue; 159 } 160 161 /// <summary> 162 /// <para>Represents a queue of timers of fixed duration.</para> 163 /// </summary> 164 private class TimerQueue : Queue 165 { 166 // This is a GCHandle that holds onto the TimerQueue when active timers are in it. 167 // The TimerThread only holds WeakReferences to it so that it can be collected when the user lets go of it. 168 // But we don't want the user to HAVE to keep a reference to it when timers are active in it. 169 // It gets created when the first timer gets added, and cleaned up when the TimerThread notices it's empty. 170 // The TimerThread will always notice it's empty eventually, since the TimerThread will always wake up and 171 // try to fire the timer, even if it was cancelled and removed prematurely. 172 private IntPtr _thisHandle; 173 174 // This sentinel TimerNode acts as both the head and the tail, allowing nodes to go in and out of the list without updating 175 // any TimerQueue members. _timers.Next is the true head, and .Prev the true tail. This also serves as the list's lock. 176 private readonly TimerNode _timers; 177 178 /// <summary> 179 /// <para>Create a new TimerQueue. TimerQueues must be created while s_NewQueues is locked in 180 /// order to synchronize with Shutdown().</para> 181 /// </summary> 182 /// <param name="durationMilliseconds"></param> TimerQueue(int durationMilliseconds)183 internal TimerQueue(int durationMilliseconds) : 184 base(durationMilliseconds) 185 { 186 // Create the doubly-linked list with a sentinel head and tail so that this member never needs updating. 187 _timers = new TimerNode(); 188 _timers.Next = _timers; 189 _timers.Prev = _timers; 190 } 191 192 /// <summary> 193 /// <para>Creates new timers. This method is thread-safe.</para> 194 /// </summary> CreateTimer(Callback callback, object context)195 internal override Timer CreateTimer(Callback callback, object context) 196 { 197 TimerNode timer = new TimerNode(callback, context, Duration, _timers); 198 199 // Add this on the tail. (Actually, one before the tail - _timers is the sentinel tail.) 200 bool needProd = false; 201 lock (_timers) 202 { 203 if (!(_timers.Prev.Next == _timers)) 204 { 205 NetEventSource.Fail(this, $"Tail corruption."); 206 } 207 208 // If this is the first timer in the list, we need to create a queue handle and prod the timer thread. 209 if (_timers.Next == _timers) 210 { 211 if (_thisHandle == IntPtr.Zero) 212 { 213 _thisHandle = (IntPtr)GCHandle.Alloc(this); 214 } 215 needProd = true; 216 } 217 218 timer.Next = _timers; 219 timer.Prev = _timers.Prev; 220 _timers.Prev.Next = timer; 221 _timers.Prev = timer; 222 } 223 224 // If, after we add the new tail, there is a chance that the tail is the next 225 // node to be processed, we need to wake up the timer thread. 226 if (needProd) 227 { 228 TimerThread.Prod(); 229 } 230 231 return timer; 232 } 233 234 /// <summary> 235 /// <para>Called by the timer thread to fire the expired timers. Returns true if there are future timers 236 /// in the queue, and if so, also sets nextExpiration.</para> 237 /// </summary> Fire(out int nextExpiration)238 internal bool Fire(out int nextExpiration) 239 { 240 while (true) 241 { 242 // Check if we got to the end. If so, free the handle. 243 TimerNode timer = _timers.Next; 244 if (timer == _timers) 245 { 246 lock (_timers) 247 { 248 timer = _timers.Next; 249 if (timer == _timers) 250 { 251 if (_thisHandle != IntPtr.Zero) 252 { 253 ((GCHandle)_thisHandle).Free(); 254 _thisHandle = IntPtr.Zero; 255 } 256 257 nextExpiration = 0; 258 return false; 259 } 260 } 261 } 262 263 if (!timer.Fire()) 264 { 265 nextExpiration = timer.Expiration; 266 return true; 267 } 268 } 269 } 270 } 271 272 /// <summary> 273 /// <para>A special dummy implementation for a queue of timers of infinite duration.</para> 274 /// </summary> 275 private class InfiniteTimerQueue : Queue 276 { InfiniteTimerQueue()277 internal InfiniteTimerQueue() : base(Timeout.Infinite) { } 278 279 /// <summary> 280 /// <para>Always returns a dummy infinite timer.</para> 281 /// </summary> CreateTimer(Callback callback, object context)282 internal override Timer CreateTimer(Callback callback, object context) => new InfiniteTimer(); 283 } 284 285 /// <summary> 286 /// <para>Internal representation of an individual timer.</para> 287 /// </summary> 288 private class TimerNode : Timer 289 { 290 private TimerState _timerState; 291 private Callback _callback; 292 private object _context; 293 private object _queueLock; 294 private TimerNode _next; 295 private TimerNode _prev; 296 297 /// <summary> 298 /// <para>Status of the timer.</para> 299 /// </summary> 300 private enum TimerState 301 { 302 Ready, 303 Fired, 304 Cancelled, 305 Sentinel 306 } 307 TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock)308 internal TimerNode(Callback callback, object context, int durationMilliseconds, object queueLock) : base(durationMilliseconds) 309 { 310 if (callback != null) 311 { 312 _callback = callback; 313 _context = context; 314 } 315 _timerState = TimerState.Ready; 316 _queueLock = queueLock; 317 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}"); 318 } 319 320 // A sentinel node - both the head and tail are one, which prevent the head and tail from ever having to be updated. TimerNode()321 internal TimerNode() : base(0) 322 { 323 _timerState = TimerState.Sentinel; 324 } 325 326 internal override bool HasExpired => _timerState == TimerState.Fired; 327 328 internal TimerNode Next 329 { 330 get { return _next; } 331 set { _next = value; } 332 } 333 334 internal TimerNode Prev 335 { 336 get { return _prev; } 337 set { _prev = value; } 338 } 339 340 /// <summary> 341 /// <para>Cancels the timer. Returns true if it hasn't and won't fire; false if it has or will, or has already been cancelled.</para> 342 /// </summary> Cancel()343 internal override bool Cancel() 344 { 345 if (_timerState == TimerState.Ready) 346 { 347 lock (_queueLock) 348 { 349 if (_timerState == TimerState.Ready) 350 { 351 // Remove it from the list. This keeps the list from getting too big when there are a lot of rapid creations 352 // and cancellations. This is done before setting it to Cancelled to try to prevent the Fire() loop from 353 // seeing it, or if it does, of having to take a lock to synchronize with the state of the list. 354 Next.Prev = Prev; 355 Prev.Next = Next; 356 357 // Just cleanup. Doesn't need to be in the lock but is easier to have here. 358 Next = null; 359 Prev = null; 360 _callback = null; 361 _context = null; 362 363 _timerState = TimerState.Cancelled; 364 365 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (success)"); 366 return true; 367 } 368 } 369 } 370 371 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (failure)"); 372 return false; 373 } 374 375 /// <summary> 376 /// <para>Fires the timer if it is still active and has expired. Returns 377 /// true if it can be deleted, or false if it is still timing.</para> 378 /// </summary> Fire()379 internal bool Fire() 380 { 381 if (_timerState == TimerState.Sentinel) 382 { 383 if (NetEventSource.IsEnabled) NetEventSource.Info(this, "TimerQueue tried to Fire a Sentinel."); 384 } 385 386 if (_timerState != TimerState.Ready) 387 { 388 return true; 389 } 390 391 // Must get the current tick count within this method so it is guaranteed not to be before 392 // StartTime, which is set in the constructor. 393 int nowMilliseconds = Environment.TickCount; 394 if (IsTickBetween(StartTime, Expiration, nowMilliseconds)) 395 { 396 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Not firing ({StartTime} <= {nowMilliseconds} < {Expiration})"); 397 return false; 398 } 399 400 bool needCallback = false; 401 lock (_queueLock) 402 { 403 if (_timerState == TimerState.Ready) 404 { 405 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Firing ({StartTime} <= {nowMilliseconds} >= " + Expiration + ")"); 406 _timerState = TimerState.Fired; 407 408 // Remove it from the list. 409 Next.Prev = Prev; 410 Prev.Next = Next; 411 412 Next = null; 413 Prev = null; 414 needCallback = _callback != null; 415 } 416 } 417 418 if (needCallback) 419 { 420 try 421 { 422 Callback callback = _callback; 423 object context = _context; 424 _callback = null; 425 _context = null; 426 callback(this, nowMilliseconds, context); 427 } 428 catch (Exception exception) 429 { 430 if (ExceptionCheck.IsFatal(exception)) 431 throw; 432 433 if (NetEventSource.IsEnabled) NetEventSource.Error(this, $"exception in callback: {exception}"); 434 435 // This thread is not allowed to go into user code, so we should never get an exception here. 436 // So, in debug, throw it up, killing the AppDomain. In release, we'll just ignore it. 437 #if DEBUG 438 throw; 439 #endif 440 } 441 } 442 443 return true; 444 } 445 } 446 447 /// <summary> 448 /// <para>A dummy infinite timer.</para> 449 /// </summary> 450 private class InfiniteTimer : Timer 451 { InfiniteTimer()452 internal InfiniteTimer() : base(Timeout.Infinite) { } 453 454 private int _cancelled; 455 456 internal override bool HasExpired => false; 457 458 /// <summary> 459 /// <para>Cancels the timer. Returns true the first time, false after that.</para> 460 /// </summary> Cancel()461 internal override bool Cancel() => Interlocked.Exchange(ref _cancelled, 1) == 0; 462 } 463 464 /// <summary> 465 /// <para>Internal mechanism used when timers are added to wake up / create the thread.</para> 466 /// </summary> Prod()467 private static void Prod() 468 { 469 s_threadReadyEvent.Set(); 470 TimerThreadState oldState = (TimerThreadState)Interlocked.CompareExchange( 471 ref s_threadState, 472 (int)TimerThreadState.Running, 473 (int)TimerThreadState.Idle); 474 475 if (oldState == TimerThreadState.Idle) 476 { 477 new Thread(new ThreadStart(ThreadProc)).Start(); 478 } 479 } 480 481 /// <summary> 482 /// <para>Thread for the timer. Ignores all exceptions. If no activity occurs for a while, 483 /// the thread will shut down.</para> 484 /// </summary> ThreadProc()485 private static void ThreadProc() 486 { 487 if (NetEventSource.IsEnabled) NetEventSource.Enter(null); 488 #if DEBUG 489 DebugThreadTracking.SetThreadSource(ThreadKinds.Timer); 490 using (DebugThreadTracking.SetThreadKind(ThreadKinds.System | ThreadKinds.Async)) 491 { 492 #endif 493 // Set this thread as a background thread. On AppDomain/Process shutdown, the thread will just be killed. 494 Thread.CurrentThread.IsBackground = true; 495 496 // Keep a permanent lock on s_Queues. This lets for example Shutdown() know when this thread isn't running. 497 lock (s_queues) 498 { 499 // If shutdown was recently called, abort here. 500 if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Running) != 501 (int)TimerThreadState.Running) 502 { 503 return; 504 } 505 506 bool running = true; 507 while (running) 508 { 509 try 510 { 511 s_threadReadyEvent.Reset(); 512 513 while (true) 514 { 515 // Copy all the new queues to the real queues. Since only this thread modifies the real queues, it doesn't have to lock it. 516 if (s_newQueues.Count > 0) 517 { 518 lock (s_newQueues) 519 { 520 for (LinkedListNode<WeakReference> node = s_newQueues.First; node != null; node = s_newQueues.First) 521 { 522 s_newQueues.Remove(node); 523 s_queues.AddLast(node); 524 } 525 } 526 } 527 528 int now = Environment.TickCount; 529 int nextTick = 0; 530 bool haveNextTick = false; 531 for (LinkedListNode<WeakReference> node = s_queues.First; node != null; /* node = node.Next must be done in the body */) 532 { 533 TimerQueue queue = (TimerQueue)node.Value.Target; 534 if (queue == null) 535 { 536 LinkedListNode<WeakReference> next = node.Next; 537 s_queues.Remove(node); 538 node = next; 539 continue; 540 } 541 542 // Fire() will always return values that should be interpreted as later than 'now' (that is, even if 'now' is 543 // returned, it is 0x100000000 milliseconds in the future). There's also a chance that Fire() will return a value 544 // intended as > 0x100000000 milliseconds from 'now'. Either case will just cause an extra scan through the timers. 545 int nextTickInstance; 546 if (queue.Fire(out nextTickInstance) && (!haveNextTick || IsTickBetween(now, nextTick, nextTickInstance))) 547 { 548 nextTick = nextTickInstance; 549 haveNextTick = true; 550 } 551 552 node = node.Next; 553 } 554 555 // Figure out how long to wait, taking into account how long the loop took. 556 // Add 15 ms to compensate for poor TickCount resolution (want to guarantee a firing). 557 int newNow = Environment.TickCount; 558 int waitDuration = haveNextTick ? 559 (int)(IsTickBetween(now, nextTick, newNow) ? 560 Math.Min(unchecked((uint)(nextTick - newNow)), (uint)(Int32.MaxValue - TickCountResolution)) + TickCountResolution : 561 0) : 562 ThreadIdleTimeoutMilliseconds; 563 564 if (NetEventSource.IsEnabled) NetEventSource.Info(null, $"Waiting for {waitDuration}ms"); 565 566 int waitResult = WaitHandle.WaitAny(s_threadEvents, waitDuration, false); 567 568 // 0 is s_ThreadShutdownEvent - die. 569 if (waitResult == 0) 570 { 571 if (NetEventSource.IsEnabled) NetEventSource.Info(null, "Awoke, cause: Shutdown"); 572 running = false; 573 break; 574 } 575 576 if (NetEventSource.IsEnabled) NetEventSource.Info(null, $"Awoke, cause {(waitResult == WaitHandle.WaitTimeout ? "Timeout" : "Prod")}"); 577 578 // If we timed out with nothing to do, shut down. 579 if (waitResult == WaitHandle.WaitTimeout && !haveNextTick) 580 { 581 Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Idle, (int)TimerThreadState.Running); 582 // There could have been one more prod between the wait and the exchange. Check, and abort if necessary. 583 if (s_threadReadyEvent.WaitOne(0, false)) 584 { 585 if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Idle) == 586 (int)TimerThreadState.Idle) 587 { 588 continue; 589 } 590 } 591 592 running = false; 593 break; 594 } 595 } 596 } 597 catch (Exception exception) 598 { 599 if (ExceptionCheck.IsFatal(exception)) 600 throw; 601 602 if (NetEventSource.IsEnabled) NetEventSource.Error(null, exception); 603 604 // The only options are to continue processing and likely enter an error-loop, 605 // shut down timers for this AppDomain, or shut down the AppDomain. Go with shutting 606 // down the AppDomain in debug, and going into a loop in retail, but try to make the 607 // loop somewhat slow. Note that in retail, this can only be triggered by OutOfMemory or StackOverflow, 608 // or an exception thrown within TimerThread - the rest are caught in Fire(). 609 #if !DEBUG 610 Thread.Sleep(1000); 611 #else 612 throw; 613 #endif 614 } 615 } 616 } 617 618 if (NetEventSource.IsEnabled) NetEventSource.Info(null, "Stop"); 619 #if DEBUG 620 } 621 #endif 622 } 623 624 /// <summary> 625 /// <para>Helper for deciding whether a given TickCount is before or after a given expiration 626 /// tick count assuming that it can't be before a given starting TickCount.</para> 627 /// </summary> IsTickBetween(int start, int end, int comparand)628 private static bool IsTickBetween(int start, int end, int comparand) 629 { 630 // Assumes that if start and end are equal, they are the same time. 631 // Assumes that if the comparand and start are equal, no time has passed, 632 // and that if the comparand and end are equal, end has occurred. 633 return ((start <= comparand) == (end <= comparand)) != (start <= end); 634 } 635 } 636 } 637