This content originally appeared on DEV Community and was authored by mapogolions
The basic abstractions
The primary abstractions in the timer mechanism are two classes within the System.Threading
namespace:
TimerQueueTimer
TimerQueue
Both classes are marked with the internal
modifier and are intended for use within the standard library to build higher-level abstractions such as System.Threading.Timer
, System.Threading.PeriodicTimer
, and System.Timers.Timer
.
TimerQueueTimer (internal class)
A timer is a wrapper object around a callable entity that is set to execute after a specified duration (dueTime
). It also supports rescheduling with a specified interval (period
).
Timers are stored in a timer queue (TimerQueue
). When a timer (TimerQueueTimer
) is created, it is associated with the timer queue (TimerQueue
) via the TimerQueueTimer._associatedTimerQueue
property.
Thus, the internal state of a timer can be represented as follows:
TimerQueueTimer
{
_associatedTimerQueue : TimerQueue
_prev : TimerQueueTimer?
_next : TimerQueueTimer?
_callback : Action<object?>
_state : object?
_dueTime : ???
_period : ???
}
TimerQueue (internal class)
This class represents a timer queue where timers (TimerQueueTimer
) are stored.
-
TimerQueue
stores timers as two doubly linked lists:-
TimerQueue._shortTimers : TimerQueueTimer
- a queue for timers with short firing time. -
TimerQueue._longTimers : TimerQueueTimer
- a queue for timers with longer firing time.
-
When adding a new timer, the threshold ShortTimersThresholdMilliseconds
, set to 333
milliseconds, is used to choose between _shortTimers
and _longTimers
.
Timers are stored as a doubly linked list, meaning each timer maintains references to the previous (
TimerQueueTimer._prev
) and next (TimerQueueTimer._next
) timers.New timers are always added to the beginning of the list, thus overwriting the previous head (
TimerQueue._shortTimers
orTimerQueue._longTimers
). The addition order doesn’t depend on the scheduled firing time of the new timer compared to existing timers in the queue.
- At any moment, the timer queue (
TimerQueue
) holds the state of the timer (TimerQueueTimer
) with the nearest firing time. Each time a new timer is added, the queue checks if the state of the soonest-to-fire timer needs updating.
For example, if a timer with a due time of TimeSpan.FromMinutes(1)
is created first, followed by a timer with a due time of TimeSpan.FromSeconds(30)
, both associated with the same timer queue (TimerQueue
), the addition of the second timer will update the soonest-to-fire timer's state.
Timer Infrastructure
The timer infrastructure refers to a set of static collections and background threads that enable the operation of timers.
When an application starts:
1) A static array of timer queues, TimerQueue.Instances : TimerQueue[]
, is created with a size of Environment.ProcessorCount
, populated with TimerQueue
objects.
2) Two static collections are created for storing timer queues (TimerQueue
). Initially, both collections are empty:
- TimerQueue.s_scheduledTimers : List<TimerQueue>
- contains scheduled timer queues. A timer queue becomes scheduled as soon as at least one timer (TimerQueueTimer
) is added to it.
- TimerQueue.s_scheduledTimersToFire : List<TimerQueue>
- holds TimerQueue
objects that are ready to execute on the thread pool, based on the nearest firing timer in each queue.
3) A background thread is created and started to monitor timer queues for readiness and schedule their execution on the thread pool. In other words, this background thread monitors the static collection TimerQueue.s_scheduledTimers
and, as needed, moves timer queue objects to the other static collection, TimerQueue.s_scheduledTimersToFire
. A more detailed explanation of the background thread's operation is provided in the Timer Scheduler
section.
The key components of the timer insfrastructure.
The above illustration is simplified. Developers aim to defer entity creation as long as possible. Only the first step above occurs immediately when the application starts. Entities in steps 2 and 3 are created only when the first timer (
TimerQueueTimer
) is registered in one of the timer queues (TimerQueue
). However, this simplification doesn’t impact understanding of the timer mechanism and eases explanation.The background thread schedules execution of the timer queue (
TimerQueue
) on the thread pool, not individual timers (TimerQueueTimer
), based on the state of the nearest firing timer within that queue. The timer queue (TimerQueue
) implements theIThreadPoolWorkItem
contract, allowing it to be executed on the thread pool.The callback associated with a timer executes on the thread pool rather than in the dedicated background thread mentioned in step 3.
Timer Scheduler
The two static collections:
TimerQueue.s_scheduledTimers
TimerQueue.s_scheduledTimersToFire
and the background thread can be conceptually grouped into a separate component called TimerScheduler
, which is responsible for scheduling the execution of timer queues on the thread pool.
The background thread's entire activity consists of moving timer queues (TimerQueue
) from one static list to another. More specifically:
- The thread repeatedly checks the nearest firing timer (
TimerQueueTimer
) in each timer queue (TimerQueue
) from the scheduled list (TimerQueue.s_scheduledTimers
) and transfers those timer queues that are ready toTimerQueue.s_scheduledTimersToFire
. - After this operation, it checks the
TimerQueue.s_scheduledTimersToFire
collection, and if it is not empty, it schedules the execution of the timer queues on the thread pool.
Here’s a simplified implementation of the above algorithm:
while (true)
{
foreach (var timerQueue in TimerQueue.s_scheduledTimers)
{
if (timerQueue.IsClosestTimerReadyToFire)
{
TimerQueue.s_scheduledTimersToFire.Add(timerQueue);
// remove `timerQueue` from s_scheduledTimers
}
}
if (TimerQueue.s_scheduledTimersToFire.Count > 0)
{
foreach (var timerQueueToFire in TimerQueue.s_scheduledTimersToFire)
{
// schedule timerQueue on thread pool
ThreadPool.UnsafeQueueHighPriorityWorkItemInternal(timerQueueToFire);
}
}
}
It is important to understand that even if a timer queue (
TimerQueue
) has been scheduled for execution on the thread pool based on the nearest firing timer's time for that queue, this does not mean that other timers (TimerQueueTimer
) stored in the same queue are ready to fire.
During execution on the thread pool, the timer queue (TimerQueue
) iterates over the two doubly linked lists (_shortTimers
and _longTimers
) of timers (TimerQueueTimer
), checking their readiness to fire. If a timer is ready, the timer itself (TimerQueueTimer
) is then scheduled for execution on the thread pool, as it also implements the IThreadPoolWorkItem
contract.
Unguaranteed Execution Order
It was previously stated that new timers are always added to the beginning of the list. This means that situations can arise where the callback for a timer with a later firing time is invoked before the callback for a timer with an earlier firing time.
There are various scenarios in which this can happen. For example, if timers created are associated with the same timer queue (TimerQueue
), and the last added timer has a slightly later firing time than the first. There is a non-zero probability that by the time the timer queue is executed on the thread pool, both timers will be ready for execution. Since the timer added last is the head of the doubly linked list, it will be attempted to schedule it first.
Creating a timer and adding it to the queue
Before moving forward, we need to introduce two informal concepts that will be used later:
-
association - this refers to the fact of assigning a reference to a selected timer queue. After the association, the timer will hold a reference to one of the timer queues from
TimerQueue.Instances
through its propertyTimerQueueTimer._associatedTimerQueue
. -
linking - this refers to the fact of adding the timer to the timer queue with which it is associated. After binding, the timer becomes part of a doubly linked list (
TimerQueue._shortTimers
orTimerQueue._longTimers
) and may (if it is not the only timer in the queue) hold references to the previous and next timers through the propertiesTimerQueueTimer._prev
andTimerQueueTimer._next
.
By the time of linking, the timer (
TimerQueueTimer
) is always associated with one of the timer queues (TimerQueue
).A timer can be associated but not linked to a timer queue.
Creating a timer with infinite firing time
To create a timer with infinite firing time, you need to specify dueTime
as Timeout.InfiniteTimeSpan
.
A timer with infinite firing time will be associated with one of the timer queues (TimerQueue
), but it will not be added to it. This fact has a consequence: if the timer queue (TimerQueue
) is empty, creating a timer with an infinite due time will not cause the timer queue to acquire the status of scheduled. In other words, the timer queue will not be placed in the static collection TimerQueue.s_scheduledTimers
, and therefore will not be monitored by the background thread.
A timer (TimerQueueTimer
) with infinite firing time can be linked to the associated timer queue (TimerQueue
), but to do this, the Change
method must be called with a finite dueTime
.
Creating a timer with finite firing time
- When a timer (
TimerQueueTimer
) is created, it is associated with one of the timer queues (TimerQueue
) through its propertyTimerQueueTimer._associatedTimerQueue
. - The created timer is added to the beginning of the list, thereby overwriting the previous head (
TimerQueue._shortTimers
orTimerQueue._longTimers
). - The associated timer queue
TimerQueue
becomes scheduled if it was not previously.
This content originally appeared on DEV Community and was authored by mapogolions
mapogolions | Sciencx (2024-10-30T21:30:16+00:00) .NET Timers Internals. Retrieved from https://www.scien.cx/2024/10/30/net-timers-internals/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.