|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace System.Threading
{
public delegate void TimerCallback(object? state);
// TimerQueue maintains a list of active timers. We use a single native timer to schedule all managed timers
// in the process.
//
// Perf assumptions: We assume that timers are created and destroyed frequently, but rarely actually fire.
// There are roughly two types of timer:
//
// - timeouts for operations. These are created and destroyed very frequently, but almost never fire, because
// the whole point is that the timer only fires if something has gone wrong.
//
// - scheduled background tasks. These typically do fire, but they usually have quite long durations.
// So the impact of spending a few extra cycles to fire these is negligible.
//
// Because of this, we want to choose a data structure with very fast insert and delete times, and we can live
// with linear traversal times when firing timers. However, we still want to minimize the number of timers
// we need to traverse while doing the linear walk: in cases where we have lots of long-lived timers as well as
// lots of short-lived timers, when the short-lived timers fire, they incur the cost of walking the long-lived ones.
//
// The data structure we've chosen is an unordered doubly-linked list of active timers. This gives O(1) insertion
// and removal, and O(N) traversal when finding expired timers. We maintain two such lists: one for all of the
// timers that'll next fire within a certain threshold, and one for the rest.
//
// Note that all instance methods of this class require that the caller hold a lock on the TimerQueue instance.
// We partition the timers across multiple TimerQueues, each with its own lock and set of short/long lists,
// in order to minimize contention when lots of threads are concurrently creating and destroying timers often.
[DebuggerDisplay("Count = {CountForDebugger}")]
[DebuggerTypeProxy(typeof(TimerQueueDebuggerTypeProxy))]
internal sealed partial class TimerQueue
{
#region Shared TimerQueue instances
/// <summary>Mapping from a tick count to a time to use when debugging to translate tick count values.</summary>
internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (TickCount64, DateTime.UtcNow);
public static TimerQueue[] Instances { get; } = CreateTimerQueues();
private static TimerQueue[] CreateTimerQueues()
{
var queues = new TimerQueue[Environment.ProcessorCount];
for (int i = 0; i < queues.Length; i++)
{
queues[i] = new TimerQueue(i);
}
return queues;
}
// This method is not thread-safe and should only be used from the debugger.
private int CountForDebugger
{
get
{
int count = 0;
foreach (TimerQueueTimer _ in GetTimersForDebugger())
{
count++;
}
return count;
}
}
// This method is not thread-safe and should only be used from the debugger.
internal IEnumerable<TimerQueueTimer> GetTimersForDebugger()
{
// This should ideally take lock(this), but doing so can hang the debugger
// if another thread holds the lock. It could instead use Monitor.TryEnter,
// but doing so doesn't work while dump debugging. So, it doesn't take the
// lock at all; it's theoretically possible but very unlikely this could result
// in a circular list that causes the debugger to hang, too.
for (TimerQueueTimer? timer = _shortTimers; timer != null; timer = timer._next)
{
yield return timer;
}
for (TimerQueueTimer? timer = _longTimers; timer != null; timer = timer._next)
{
yield return timer;
}
}
private sealed class TimerQueueDebuggerTypeProxy
{
private readonly TimerQueue _queue;
public TimerQueueDebuggerTypeProxy(TimerQueue queue)
{
ArgumentNullException.ThrowIfNull(queue);
_queue = queue;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public TimerQueueTimer[] Items => new List<TimerQueueTimer>(_queue.GetTimersForDebugger()).ToArray();
}
#endregion
#region interface to native timer
private bool _isTimerScheduled;
private long _currentTimerStartTicks;
private uint _currentTimerDuration;
private bool EnsureTimerFiresBy(uint requestedDuration)
{
// The VM's timer implementation does not work well for very long-duration timers.
// So we limit our native timer duration to a "small" value.
// This may cause us to attempt to fire timers early, but that's ok -
// we'll just see that none of our timers has actually reached its due time,
// and schedule the native timer again.
const uint maxPossibleDuration = 0x0fffffff;
uint actualDuration = Math.Min(requestedDuration, maxPossibleDuration);
if (_isTimerScheduled)
{
long elapsed = TickCount64 - _currentTimerStartTicks;
if (elapsed >= _currentTimerDuration)
return true; // the timer's about to fire
uint remainingDuration = _currentTimerDuration - (uint)elapsed;
if (actualDuration >= remainingDuration)
return true; // the timer will fire earlier than this request
}
if (SetTimer(actualDuration))
{
_isTimerScheduled = true;
_currentTimerStartTicks = TickCount64;
_currentTimerDuration = actualDuration;
return true;
}
return false;
}
#endregion
#region Firing timers
// The two lists of timers that are part of this TimerQueue. They conform to a single guarantee:
// no timer in _longTimers has an absolute next firing time <= _currentAbsoluteThreshold.
// That way, when FireNextTimers is invoked, we always process the short list, and we then only
// process the long list if the current time is greater than _currentAbsoluteThreshold (or
// if the short list is now empty and we need to process the long list to know when to next
// invoke FireNextTimers).
private TimerQueueTimer? _shortTimers;
private TimerQueueTimer? _longTimers;
// The current threshold, an absolute time where any timers scheduled to go off at or
// before this time must be queued to the short list.
private long _currentAbsoluteThreshold = TickCount64 + ShortTimersThresholdMilliseconds;
// Default threshold that separates which timers target _shortTimers vs _longTimers. The threshold
// is chosen to balance the number of timers in the small list against the frequency with which
// we need to scan the long list. It's thus somewhat arbitrary and could be changed based on
// observed workload demand. The larger the number, the more timers we'll likely need to enumerate
// every time the timer fires, but also the more likely it is that when it does we won't
// need to look at the long list because the current time will be <= _currentAbsoluteThreshold.
private const int ShortTimersThresholdMilliseconds = 333;
// Fire any timers that have expired, and update the native timer to schedule the rest of them.
// We're in a thread pool work item here, and if there are multiple timers to be fired, we want
// to queue all but the first one. The first may can then be invoked synchronously or queued,
// a task left up to our caller, which might be firing timers from multiple queues.
private void FireNextTimers()
{
// We fire the first timer on this thread; any other timers that need to be fired
// are queued to the ThreadPool.
TimerQueueTimer? timerToFireOnThisThread = null;
lock (this)
{
// Since we got here, that means our previous timer has fired.
_isTimerScheduled = false;
bool haveTimerToSchedule = false;
uint nextTimerDuration = uint.MaxValue;
long nowTicks = TickCount64;
// Sweep through the "short" timers. If the current tick count is greater than
// the current threshold, also sweep through the "long" timers. Finally, as part
// of sweeping the long timers, move anything that'll fire within the next threshold
// to the short list. It's functionally ok if more timers end up in the short list
// than is truly necessary (but not the opposite).
TimerQueueTimer? timer = _shortTimers;
for (int listNum = 0; listNum < 2; listNum++) // short == 0, long == 1
{
while (timer != null)
{
Debug.Assert(timer._dueTime != Timeout.UnsignedInfinite, "A timer in the list must have a valid due time.");
// Save off the next timer to examine, in case our examination of this timer results
// in our deleting or moving it; we'll continue after with this saved next timer.
TimerQueueTimer? next = timer._next;
long elapsed = nowTicks - timer._startTicks;
long remaining = timer._dueTime - elapsed;
if (remaining <= 0)
{
// Timer is ready to fire.
timer._everQueued = true;
if (timer._period != Timeout.UnsignedInfinite)
{
// This is a repeating timer; schedule it to run again.
// Discount the extra amount of time that has elapsed since the previous firing time to
// prevent timer ticks from drifting. If enough time has already elapsed for the timer to fire
// again, meaning the timer can't keep up with the short period, have it fire 1 ms from now to
// avoid spinning without a delay.
timer._startTicks = nowTicks;
long elapsedForNextDueTime = elapsed - timer._dueTime;
timer._dueTime = (elapsedForNextDueTime < timer._period) ?
timer._period - (uint)elapsedForNextDueTime :
1;
// Update the timer if this becomes the next timer to fire.
if (timer._dueTime < nextTimerDuration)
{
haveTimerToSchedule = true;
nextTimerDuration = timer._dueTime;
}
// Validate that the repeating timer is still on the right list. It's likely that
// it started in the long list and was moved to the short list at some point, so
// we now want to move it back to the long list if that's where it belongs. Note that
// if we're currently processing the short list and move it to the long list, we may
// end up revisiting it again if we also enumerate the long list, but we will have already
// updated the due time appropriately so that we won't fire it again (it's also possible
// but rare that we could be moving a timer from the long list to the short list here,
// if the initial due time was set to be long but the timer then had a short period).
bool targetShortList = (nowTicks + timer._dueTime) - _currentAbsoluteThreshold <= 0;
if (timer._short != targetShortList)
{
MoveTimerToCorrectList(timer, targetShortList);
}
}
else
{
// Not repeating; remove it from the queue
DeleteTimer(timer);
}
// If this is the first timer, we'll fire it on this thread (after processing
// all others). Otherwise, queue it to the ThreadPool.
if (timerToFireOnThisThread == null)
{
timerToFireOnThisThread = timer;
}
else
{
ThreadPool.UnsafeQueueUserWorkItemInternal(timer, preferLocal: false);
}
}
else
{
// This timer isn't ready to fire. Update the next time the native timer fires if necessary,
// and move this timer to the short list if its remaining time is now at or under the threshold.
if (remaining < nextTimerDuration)
{
haveTimerToSchedule = true;
nextTimerDuration = (uint)remaining;
}
if (!timer._short && remaining <= ShortTimersThresholdMilliseconds)
{
MoveTimerToCorrectList(timer, shortList: true);
}
}
timer = next;
}
// Switch to process the long list if necessary.
if (listNum == 0)
{
// Determine how much time remains between now and the current threshold. If time remains,
// we can skip processing the long list. We use > rather than >= because, although we
// know that if remaining == 0 no timers in the long list will need to be fired, we
// don't know without looking at them when we'll need to call FireNextTimers again. We
// could in that case just set the next firing to 1, but we may as well just iterate the
// long list now; otherwise, most timers created in the interim would end up in the long
// list and we'd likely end up paying for another invocation of FireNextTimers that could
// have been delayed longer (to whatever is the current minimum in the long list).
long remaining = _currentAbsoluteThreshold - nowTicks;
if (remaining > 0)
{
if (_shortTimers == null && _longTimers != null)
{
// We don't have any short timers left and we haven't examined the long list,
// which means we likely don't have an accurate nextTimerDuration.
// But we do know that nothing in the long list will be firing before or at _currentAbsoluteThreshold,
// so we can just set nextTimerDuration to the difference between then and now.
nextTimerDuration = (uint)remaining + 1;
haveTimerToSchedule = true;
}
break;
}
// Switch to processing the long list.
timer = _longTimers;
// Now that we're going to process the long list, update the current threshold.
_currentAbsoluteThreshold = nowTicks + ShortTimersThresholdMilliseconds;
}
}
// If we still have scheduled timers, update the timer to ensure it fires
// in time for the next one in line.
if (haveTimerToSchedule)
{
EnsureTimerFiresBy(nextTimerDuration);
}
}
// Fire the user timer outside of the lock!
timerToFireOnThisThread?.Fire();
}
#endregion
#region Queue implementation
public long ActiveCount { get; private set; }
public bool UpdateTimer(TimerQueueTimer timer, uint dueTime, uint period)
{
long nowTicks = TickCount64;
// The timer can be put onto the short list if it's next absolute firing time
// is <= the current absolute threshold.
long absoluteDueTime = nowTicks + dueTime;
bool shouldBeShort = _currentAbsoluteThreshold - absoluteDueTime >= 0;
if (timer._dueTime == Timeout.UnsignedInfinite)
{
// If the timer wasn't previously scheduled, now add it to the right list.
timer._short = shouldBeShort;
LinkTimer(timer);
++ActiveCount;
}
else if (timer._short != shouldBeShort)
{
// If the timer was previously scheduled, but this update should cause
// it to move over the list threshold in either direction, do so.
UnlinkTimer(timer);
timer._short = shouldBeShort;
LinkTimer(timer);
}
timer._dueTime = dueTime;
timer._period = (period == 0) ? Timeout.UnsignedInfinite : period;
timer._startTicks = nowTicks;
return EnsureTimerFiresBy(dueTime);
}
public void MoveTimerToCorrectList(TimerQueueTimer timer, bool shortList)
{
Debug.Assert(timer._dueTime != Timeout.UnsignedInfinite, "Expected timer to be on a list.");
Debug.Assert(timer._short != shortList, "Unnecessary if timer is already on the right list.");
// Unlink it from whatever list it's on, change its list association, then re-link it.
UnlinkTimer(timer);
timer._short = shortList;
LinkTimer(timer);
}
private void LinkTimer(TimerQueueTimer timer)
{
// Use timer._short to decide to which list to add.
ref TimerQueueTimer? listHead = ref timer._short ? ref _shortTimers : ref _longTimers;
timer._next = listHead;
if (timer._next != null)
{
timer._next._prev = timer;
}
timer._prev = null;
listHead = timer;
}
private void UnlinkTimer(TimerQueueTimer timer)
{
TimerQueueTimer? t = timer._next;
if (t != null)
{
t._prev = timer._prev;
}
if (_shortTimers == timer)
{
Debug.Assert(timer._short);
_shortTimers = t;
}
else if (_longTimers == timer)
{
Debug.Assert(!timer._short);
_longTimers = t;
}
t = timer._prev;
if (t != null)
{
t._next = timer._next;
}
// At this point the timer is no longer in a list, but its next and prev
// references may still point to other nodes. UnlinkTimer should thus be
// followed by something that overwrites those references, either with null
// if deleting the timer or other nodes if adding it to another list.
}
public void DeleteTimer(TimerQueueTimer timer)
{
if (timer._dueTime != Timeout.UnsignedInfinite)
{
--ActiveCount;
Debug.Assert(ActiveCount >= 0);
UnlinkTimer(timer);
timer._prev = null;
timer._next = null;
timer._dueTime = Timeout.UnsignedInfinite;
timer._period = Timeout.UnsignedInfinite;
timer._startTicks = 0;
timer._short = false;
}
}
#endregion
}
// A timer in our TimerQueue.
[DebuggerDisplay("{DisplayString,nq}")]
[DebuggerTypeProxy(typeof(TimerDebuggerTypeProxy))]
internal sealed class TimerQueueTimer : ITimer, IThreadPoolWorkItem
{
// The associated timer queue.
private readonly TimerQueue _associatedTimerQueue;
// All mutable fields of this class are protected by a lock on _associatedTimerQueue.
// The first six fields are maintained by TimerQueue.
// Links to the next and prev timers in the list.
internal TimerQueueTimer? _next;
internal TimerQueueTimer? _prev;
// true if on the short list; otherwise, false.
internal bool _short;
// The time, according to TimerQueue.TickCount, when this timer's current interval started.
internal long _startTicks;
// Timeout.UnsignedInfinite if we are not going to fire. Otherwise, the offset from _startTime when we will fire.
internal uint _dueTime;
// Timeout.UnsignedInfinite if we are a single-shot timer. Otherwise, the repeat interval.
internal uint _period;
// Info about the user's callback
private readonly TimerCallback _timerCallback;
private readonly object? _state;
private readonly ExecutionContext? _executionContext;
// When Timer.Dispose(WaitHandle) is used, we need to signal the wait handle only
// after all pending callbacks are complete. We set _canceled to prevent any callbacks that
// are already queued from running. We track the number of callbacks currently executing in
// _callbacksRunning. We set _notifyWhenNoCallbacksRunning only when _callbacksRunning
// reaches zero. Same applies if Timer.DisposeAsync() is used, except with a Task
// instead of with a provided WaitHandle.
private int _callbacksRunning;
private bool _canceled;
internal bool _everQueued;
private object? _notifyWhenNoCallbacksRunning; // may be either WaitHandle or Task
internal TimerQueueTimer(TimerCallback timerCallback, object? state, TimeSpan dueTime, TimeSpan period, bool flowExecutionContext) :
this(timerCallback, state, GetMilliseconds(dueTime), GetMilliseconds(period), flowExecutionContext)
{
}
private static uint GetMilliseconds(TimeSpan time, [CallerArgumentExpression(nameof(time))] string? parameter = null)
{
long tm = (long)time.TotalMilliseconds;
ArgumentOutOfRangeException.ThrowIfLessThan(tm, -1, parameter);
ArgumentOutOfRangeException.ThrowIfGreaterThan(tm, Timer.MaxSupportedTimeout, parameter);
return (uint)tm;
}
internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTime, uint period, bool flowExecutionContext)
{
_timerCallback = timerCallback;
_state = state;
_dueTime = Timeout.UnsignedInfinite;
_period = Timeout.UnsignedInfinite;
if (flowExecutionContext)
{
_executionContext = ExecutionContext.Capture();
}
_associatedTimerQueue = TimerQueue.Instances[(uint)Thread.GetCurrentProcessorId() % TimerQueue.Instances.Length];
// After the following statement, the timer may fire. No more manipulation of timer state outside of
// the lock is permitted beyond this point!
if (dueTime != Timeout.UnsignedInfinite)
Change(dueTime, period);
}
internal string DisplayString
{
get
{
string? typeName = _timerCallback.Method.DeclaringType?.FullName;
if (typeName is not null)
{
typeName += ".";
}
return
"DueTime = " + (_dueTime == Timeout.UnsignedInfinite ? "(not set)" : TimeSpan.FromMilliseconds(_dueTime)) + ", " +
"Period = " + (_period == Timeout.UnsignedInfinite ? "(not set)" : TimeSpan.FromMilliseconds(_period)) + ", " +
typeName + _timerCallback.Method.Name + "(" + (_state?.ToString() ?? "null") + ")";
}
}
public bool Change(TimeSpan dueTime, TimeSpan period) =>
Change(GetMilliseconds(dueTime), GetMilliseconds(period));
internal bool Change(uint dueTime, uint period)
{
bool success;
lock (_associatedTimerQueue)
{
if (_canceled)
{
return false;
}
_period = period;
if (dueTime == Timeout.UnsignedInfinite)
{
_associatedTimerQueue.DeleteTimer(this);
success = true;
}
else
{
if (FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer))
FrameworkEventSource.Log.ThreadTransferSendObj(this, 1, string.Empty, true, (int)dueTime, (int)period);
success = _associatedTimerQueue.UpdateTimer(this, dueTime, period);
}
}
return success;
}
public void Dispose()
{
lock (_associatedTimerQueue)
{
if (!_canceled)
{
_canceled = true;
_associatedTimerQueue.DeleteTimer(this);
}
}
}
public bool Dispose(WaitHandle toSignal)
{
Debug.Assert(toSignal != null);
bool success;
bool shouldSignal = false;
lock (_associatedTimerQueue)
{
if (_canceled)
{
success = false;
}
else
{
_canceled = true;
_notifyWhenNoCallbacksRunning = toSignal;
_associatedTimerQueue.DeleteTimer(this);
shouldSignal = _callbacksRunning == 0;
success = true;
}
}
if (shouldSignal)
SignalNoCallbacksRunning();
return success;
}
public ValueTask DisposeAsync()
{
lock (_associatedTimerQueue)
{
object? notifyWhenNoCallbacksRunning = _notifyWhenNoCallbacksRunning;
// Mark the timer as canceled if it's not already.
if (_canceled)
{
if (notifyWhenNoCallbacksRunning is WaitHandle)
{
// A previous call to Close(WaitHandle) stored a WaitHandle. We could try to deal with
// this case by using ThreadPool.RegisterWaitForSingleObject to create a Task that'll
// complete when the WaitHandle is set, but since arbitrary WaitHandle's can be supplied
// by the caller, it could be for an auto-reset event or similar where that caller's
// WaitOne on the WaitHandle could prevent this wrapper Task from completing. We could also
// change the implementation to support storing multiple objects, but that's not pay-for-play,
// and the existing Close(WaitHandle) already discounts this as being invalid, instead just
// returning false if you use it multiple times. Since first calling Timer.Dispose(WaitHandle)
// and then calling Timer.DisposeAsync is not something anyone is likely to or should do, we
// simplify by just failing in that case.
var e = new InvalidOperationException(SR.InvalidOperation_TimerAlreadyClosed);
e.SetCurrentStackTrace();
return ValueTask.FromException(e);
}
}
else
{
_canceled = true;
_associatedTimerQueue.DeleteTimer(this);
}
// We've deleted the timer, so if there are no callbacks queued or running,
// we're done and return an already-completed value task.
if (_callbacksRunning == 0)
{
return default;
}
Debug.Assert(
notifyWhenNoCallbacksRunning == null ||
notifyWhenNoCallbacksRunning is Task);
// There are callbacks queued or running, so we need to store a Task
// that'll be used to signal the caller when all callbacks complete. Do so as long as
// there wasn't a previous CloseAsync call that did.
if (notifyWhenNoCallbacksRunning == null)
{
var t = new Task((object?)null, TaskCreationOptions.RunContinuationsAsynchronously, true);
_notifyWhenNoCallbacksRunning = t;
return new ValueTask(t);
}
// A previous CloseAsync call already hooked up a task. Just return it.
return new ValueTask((Task)notifyWhenNoCallbacksRunning);
}
}
void IThreadPoolWorkItem.Execute() => Fire(isThreadPool: true);
internal void Fire(bool isThreadPool = false)
{
bool canceled = false;
lock (_associatedTimerQueue)
{
canceled = _canceled;
if (!canceled)
_callbacksRunning++;
}
if (canceled)
return;
CallCallback(isThreadPool);
bool shouldSignal;
lock (_associatedTimerQueue)
{
_callbacksRunning--;
shouldSignal = _canceled && _callbacksRunning == 0 && _notifyWhenNoCallbacksRunning != null;
}
if (shouldSignal)
SignalNoCallbacksRunning();
}
internal void SignalNoCallbacksRunning()
{
object? toSignal = _notifyWhenNoCallbacksRunning;
Debug.Assert(toSignal is WaitHandle || toSignal is Task);
if (toSignal is WaitHandle wh)
{
EventWaitHandle.Set(wh.SafeWaitHandle);
}
else
{
((Task)toSignal).TrySetResult();
}
}
internal void CallCallback(bool isThreadPool)
{
if (FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer))
FrameworkEventSource.Log.ThreadTransferReceiveObj(this, 1, string.Empty);
// Call directly if EC flow is suppressed
ExecutionContext? context = _executionContext;
if (context == null)
{
_timerCallback(_state);
}
else
{
if (isThreadPool)
{
ExecutionContext.RunFromThreadPoolDispatchLoop(Thread.CurrentThread, context, s_callCallbackInContext, this);
}
else
{
ExecutionContext.RunInternal(context, s_callCallbackInContext, this);
}
}
}
private static readonly ContextCallback s_callCallbackInContext = static state =>
{
Debug.Assert(state is TimerQueueTimer);
var t = (TimerQueueTimer)state;
t._timerCallback(t._state);
};
internal sealed class TimerDebuggerTypeProxy
{
private readonly TimerQueueTimer _timer;
public TimerDebuggerTypeProxy(Timer timer) => _timer = timer._timer._timer;
public TimerDebuggerTypeProxy(TimerQueueTimer timer) => _timer = timer;
public DateTime? EstimatedNextTimeUtc
{
get
{
if (_timer._dueTime != Timeout.UnsignedInfinite)
{
// In TimerQueue's static ctor, we snap a tick count and the current time, as a way of being
// able to translate from tick counts to times. This is only approximate, for a variety of
// reasons (e.g. drift, clock changes, etc.), but when dump debugging we are unable to use
// TickCount in a meaningful way, so this at least provides a reasonable approximation.
long msOffset = _timer._startTicks - TimerQueue.s_tickCountToTimeMap.TickCount + _timer._dueTime;
return (TimerQueue.s_tickCountToTimeMap.Time + TimeSpan.FromMilliseconds(msOffset));
}
return null;
}
}
public TimeSpan? DueTime => _timer._dueTime == Timeout.UnsignedInfinite ? null : TimeSpan.FromMilliseconds(_timer._dueTime);
public TimeSpan? Period => _timer._period == Timeout.UnsignedInfinite ? null : TimeSpan.FromMilliseconds(_timer._period);
public TimerCallback Callback => _timer._timerCallback;
public object? State => _timer._state;
}
}
// TimerHolder serves as an intermediary between Timer and TimerQueueTimer, releasing the TimerQueueTimer
// if the Timer is collected.
// This is necessary because Timer itself cannot use its finalizer for this purpose. If it did,
// then users could control timer lifetimes using GC.SuppressFinalize/ReRegisterForFinalize.
// You might ask, wouldn't that be a good thing? Maybe (though it would be even better to offer this
// via first-class APIs), but Timer has never offered this, and adding it now would be a breaking
// change, because any code that happened to be suppressing finalization of Timer objects would now
// unwittingly be changing the lifetime of those timers.
internal sealed class TimerHolder
{
internal readonly TimerQueueTimer _timer;
public TimerHolder(TimerQueueTimer timer)
{
_timer = timer;
}
~TimerHolder()
{
_timer.Dispose();
}
public void Dispose()
{
_timer.Dispose();
GC.SuppressFinalize(this);
}
public bool Dispose(WaitHandle notifyObject)
{
bool result = _timer.Dispose(notifyObject);
GC.SuppressFinalize(this);
return result;
}
public ValueTask DisposeAsync()
{
ValueTask result = _timer.DisposeAsync();
GC.SuppressFinalize(this);
return result;
}
}
[DebuggerDisplay("{DisplayString,nq}")]
[DebuggerTypeProxy(typeof(TimerQueueTimer.TimerDebuggerTypeProxy))]
public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable, ITimer
{
internal const uint MaxSupportedTimeout = 0xfffffffe;
internal TimerHolder _timer;
public Timer(TimerCallback callback,
object? state,
int dueTime,
int period) :
this(callback, state, dueTime, period, flowExecutionContext: true)
{
}
internal Timer(TimerCallback callback,
object? state,
int dueTime,
int period,
bool flowExecutionContext)
{
ArgumentOutOfRangeException.ThrowIfLessThan(dueTime, -1);
ArgumentOutOfRangeException.ThrowIfLessThan(period, -1);
TimerSetup(callback, state, (uint)dueTime, (uint)period, flowExecutionContext);
}
public Timer(TimerCallback callback,
object? state,
TimeSpan dueTime,
TimeSpan period)
{
long dueTm = (long)dueTime.TotalMilliseconds;
ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime));
ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, MaxSupportedTimeout, nameof(dueTime));
long periodTm = (long)period.TotalMilliseconds;
ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(period));
ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, MaxSupportedTimeout, nameof(period));
TimerSetup(callback, state, (uint)dueTm, (uint)periodTm);
}
[CLSCompliant(false)]
public Timer(TimerCallback callback,
object? state,
uint dueTime,
uint period)
{
TimerSetup(callback, state, dueTime, period);
}
public Timer(TimerCallback callback,
object? state,
long dueTime,
long period)
{
ArgumentOutOfRangeException.ThrowIfLessThan(dueTime, -1);
ArgumentOutOfRangeException.ThrowIfLessThan(period, -1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTime, MaxSupportedTimeout);
ArgumentOutOfRangeException.ThrowIfGreaterThan(period, MaxSupportedTimeout);
TimerSetup(callback, state, (uint)dueTime, (uint)period);
}
public Timer(TimerCallback callback)
{
const uint DueTime = unchecked((uint)(-1)); // We want timer to be registered, but not activated. Requires caller to call
const uint Period = unchecked((uint)(-1)); // Change after a timer instance is created. This is to avoid the potential
// for a timer to be fired before the returned value is assigned to the variable,
// potentially causing the callback to reference a bogus value (if passing the timer to the callback).
TimerSetup(callback, this, DueTime, Period);
}
[MemberNotNull(nameof(_timer))]
private void TimerSetup(TimerCallback callback,
object? state,
uint dueTime,
uint period,
bool flowExecutionContext = true)
{
ArgumentNullException.ThrowIfNull(callback);
_timer = new TimerHolder(new TimerQueueTimer(callback, state, dueTime, period, flowExecutionContext));
}
public bool Change(int dueTime, int period)
{
ArgumentOutOfRangeException.ThrowIfLessThan(dueTime, -1);
ArgumentOutOfRangeException.ThrowIfLessThan(period, -1);
return _timer._timer.Change((uint)dueTime, (uint)period);
}
public bool Change(TimeSpan dueTime, TimeSpan period) =>
_timer._timer.Change(dueTime, period);
[CLSCompliant(false)]
public bool Change(uint dueTime, uint period)
{
return _timer._timer.Change(dueTime, period);
}
public bool Change(long dueTime, long period)
{
ArgumentOutOfRangeException.ThrowIfLessThan(dueTime, -1);
ArgumentOutOfRangeException.ThrowIfLessThan(period, -1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTime, MaxSupportedTimeout);
ArgumentOutOfRangeException.ThrowIfGreaterThan(period, MaxSupportedTimeout);
return _timer._timer.Change((uint)dueTime, (uint)period);
}
/// <summary>
/// Gets the number of timers that are currently active. An active timer is registered to tick at some point in the
/// future, and has not yet been canceled.
/// </summary>
public static long ActiveCount
{
get
{
long count = 0;
foreach (TimerQueue queue in TimerQueue.Instances)
{
lock (queue)
{
count += queue.ActiveCount;
}
}
return count;
}
}
public bool Dispose(WaitHandle notifyObject)
{
ArgumentNullException.ThrowIfNull(notifyObject);
return _timer.Dispose(notifyObject);
}
public void Dispose()
{
_timer.Dispose();
}
public ValueTask DisposeAsync()
{
return _timer.DisposeAsync();
}
private string DisplayString => _timer._timer.DisplayString;
/// <summary>Gets a list of all timers for debugging purposes.</summary>
private static IEnumerable<TimerQueueTimer> AllTimers // intended to be used by devs from debugger
{
get
{
var timers = new List<TimerQueueTimer>();
foreach (TimerQueue queue in TimerQueue.Instances)
{
timers.AddRange(queue.GetTimersForDebugger());
}
timers.Sort((t1, t2) => t1._dueTime.CompareTo(t2._dueTime));
return timers;
}
}
}
}
|