Timers are one of those fuzzy topics. Most programmers know how to use them and the various implementations. But, how do they actually work?
Behind the scenes there's only one running timer.
TimerQueue
implements the Singleton pattern : one instance per AppDomain. It manages all the application timers and schedules the callbacks when timers expire.
When you create a timer, you queue a timer object to TimerQueue
.
Timers are created for two purposes:
Operational Timeouts. These are system timers created and destroyed frequently. They only fire if something goes wrong.
Scheduled Background Tasks. These are designed to fire. They include all the timers we create and consume in our code.
TimerQueue
runs on it's own thread. The singleton creator method does this:
new Thread(TimerQueue.Create().Run());
TimerQueue
uses a single native timer provided by the underlying operating system, normally provided by the Virtual Machine.
The basic operation can be summarised as:
void Resume()
{
FireAllExpiredTimers();
nextTimeSpan = UnexpiredTimers.GetShortestTimeSpanToExpiration();
NativeTimer.ScheduleCallback(Resume, nextTimeSpan);
}
Resume
:
When complete it schedules a callback to itself from the Native timer for the minumim period. Note that the resolution of nextTimeSpan
will be based on the resolution of the system clock: approx 15 ms on a Windows Server.
Adding a new timer looks like this:
void AddTimer(Timer timer)
{
AddTimerToList(timer);
ResheduleNativeTimerIfRequired(Resume, nextTimeSpan);
}
This adds timer
to the queue and reschedules the native timer callback if the new timer's timespan is shorter than the current scheduled callback.
When you "run" a timer there's no black magic. You add a new timer to the queue and carry on to the next task: fire and forget. There's nothing happening "in the background" on your current execution thread. The tracking, management and callbacks are managed by TimerQueue
running on its own thread.
You should Dispose
a timer to remove it from the queue.
The callback (or event in a System.Timers.Timer object) runs on a threadpool context, not the context of the owning object. There's no automatic detection and switching to the synchronisation context.