.NET Runtime 中 TimerQueue 与系统定时器的协作机制
TimerQueue 的核心调度逻辑
在 .NET 运行时中,TimerQueue 承担着管理定时任务并与操作系统底层定时器交互的关键职责。每个 TimerQueue 实例负责维护一组 TimerQueueTimer 对象,这些对象代表了具体的定时任务。为了高效处理时间事件,Runtime 会根据 CPU 核心数创建对应数量的 TimerQueue 实例,并将任务分散到不同的队列中。
TimerQueue 内部维护了两个双向链表,分别存储短期任务(shortTimers)和长期任务(longTimers)。队列会根据任务的到期时间动态调整,并计算出当前队列中最早需要执行的任务时间。这个最早时间点将被注册到操作系统的定时器机制中,以便在到期时获得通知。
定时器的按需注册策略
TimerQueue 并不总是立即向操作系统注册定时器,而是采用按需注册的策略。这一逻辑主要封装在内部方法中,用于判断当前是否已经存在有效的定时器注册,或者是否需要更新现有的注册信息。
该方法主要在两个场景下被触发:一是当新的定时任务被创建或更新时;二是当当前队列中的任务执行完毕后,检查是否还有剩余未到期任务需要继续监听。为了适应底层限制,运行时会将请求的持续时间限制在特定范围内(约 3.11 天),避免因时间过长导致底层实现异常。
以下是经过简化的逻辑实现示例,展示了如何判断是否需要重新注册定时器:
internal class TimerQueue : IThreadPoolWorkItem
{
private bool _timerRegistered;
private long _registrationTick;
private uint _registeredDuration;
private bool ValidateAndRegisterTimer(uint requestedDuration)
{
// 限制最大持续时间,防止底层定时器溢出
const uint maxDurationLimit = 0x0fffffff;
uint effectiveDuration = Math.Min(requestedDuration, maxDurationLimit);
if (_timerRegistered)
{
// 计算已经经过的时间
long elapsed = TickCount64 - _registrationTick;
// 如果当前定时器已到期,视为需要重新注册
if (elapsed >= _registeredDuration)
return true;
// 计算剩余时间
uint remaining = _registeredDuration - (uint)elapsed;
// 如果新请求的时间晚于当前剩余时间,无需更新
if (effectiveDuration >= remaining)
return true;
}
// 执行底层注册
if (RegisterSystemTimer(effectiveDuration))
{
_timerRegistered = true;
_registrationTick = TickCount64;
_registeredDuration = effectiveDuration;
return true;
}
return false;
}
}
通过上述逻辑,TimerQueue 能够有效地减少与操作系统交互的频率,仅在必要时更新定时器注册信息,从而降低系统开销。
基于 AutoResetEvent 的同步机制
为了等待操作系统定时器的信号,TimerQueue 利用 AutoResetEvent 这一线程同步基元进行封装。AutoResetEvent 内部维护了一个内核对象,具备 signaled(终止)和 non-signaled(非终止)两种状态。
当线程调用 WaitOne 方法时,如果对象处于非终止状态,线程将进入阻塞等待。一旦对象状态变为终止状态(通过 Set 方法触发),等待的线程将被唤醒,且对象状态自动重置为非终止状态。这种机制非常适合用于单次通知场景。
以下示例展示了 AutoResetEvent 的基本行为模式:
var signal = new AutoResetEvent(initialState: false);
Task.Run(() =>
{
Console.WriteLine($"Worker started at {DateTime.Now:HH:mm:ss}");
// 阻塞等待信号
signal.WaitOne();
Console.WriteLine($"Signal received at {DateTime.Now:HH:mm:ss}");
// 再次等待,需要再次触发 Set
signal.WaitOne();
Console.WriteLine($"Second signal at {DateTime.Now:HH:mm:ss}");
});
// 模拟延迟触发
Thread.Sleep(1000);
signal.Set();
Thread.Sleep(2000);
signal.Set();
此外,WaitOne 方法支持传入超时参数。如果在指定时间内未收到信号,线程会自动解除阻塞继续执行,这为定时器管理提供了超时保护机制。
全局定时器管理线程
在 .NET 7 及更高版本中,所有的 TimerQueue 实例共享一个专用的后台线程用于处理定时器到期事件。这个管理线程通过监听全局的 AutoResetEvent 来唤醒,并负责遍历所有活跃的 TimerQueue 实例。
管理流程主要涉及三个核心部分:定时器的注册入口、管理器的初始化以及后台处理循环。当某个 TimerQueue 需要注册定时器时,它会将自己添加到全局列表中,并触发全局信号量唤醒管理线程。
internal class TimerQueue : IThreadPoolWorkItem
{
private static List<TimerQueue>? _globalActiveQueues;
private static List<TimerQueue>? _queuesReadyToFire;
// 全局同步事件,用于唤醒管理线程
private static readonly AutoResetEvent _globalWakeSignal = new AutoResetEvent(false);
private bool _isInGlobalList;
private long _dueTimeMs;
private bool RegisterSystemTimer(uint duration)
{
long targetTime = TickCount64 + (int)duration;
lock (_globalWakeSignal)
{
if (!_isInGlobalList)
{
var list = _globalActiveQueues ?? InitializeManager();
list.Add(this);
_isInGlobalList = true;
}
_dueTimeMs = targetTime;
}
// 唤醒管理线程进行检查
_globalWakeSignal.Set();
return true;
}
private static List<TimerQueue> InitializeManager()
{
var list = new List<TimerQueue>(TimerQueue.Instances.Length);
_queuesReadyToFire ??= new List<TimerQueue>(TimerQueue.Instances.Length);
var thread = new Thread(ProcessTimerEvents)
{
Name = ".NET Timer Manager",
IsBackground = true
};
thread.UnsafeStart();
_globalActiveQueues = list;
return list;
}
private static void ProcessTimerEvents()
{
var signal = _globalWakeSignal;
var fireList = _queuesReadyToFire!;
List<TimerQueue> activeList;
lock (signal)
{
activeList = _globalActiveQueues!;
}
int minWaitTime = Timeout.Infinite;
while (true)
{
// 等待信号或超时
signal.WaitOne(minWaitTime);
long now = TickCount64;
minWaitTime = int.MaxValue;
lock (signal)
{
for (int i = activeList.Count - 1; i >= 0; --i)
{
var queue = activeList[i];
long remaining = queue._dueTimeMs - now;
if (remaining <= 0)
{
// 任务已到期,移出活跃列表
queue._isInGlobalList = false;
fireList.Add(queue);
// 优化列表移除操作
int last = activeList.Count - 1;
if (i != last)
activeList[i] = activeList[last];
activeList.RemoveAt(last);
continue;
}
// 更新最短等待时间
if (remaining < minWaitTime)
minWaitTime = (int)remaining;
}
}
// 将到期的队列提交到线程池
if (fireList.Count > 0)
{
foreach (var q in fireList)
{
// 使用高优先级队列提交任务
ThreadPool.UnsafeQueueHighPriorityWorkItemInternal(q);
}
fireList.Clear();
}
if (minWaitTime == int.MaxValue)
minWaitTime = Timeout.Infinite;
}
}
void IThreadPoolWorkItem.Execute() => ProcessExpiredTimers();
}
管理线程在唤醒后,会计算所有活跃队列的最短剩余时间,并以此作为下一次等待的超时值。这种机制确保了定时器能够在精确的时间点被触发,同时避免了忙等待造成的 CPU 浪费。当 TimerQueue 被提交到线程池后,它会执行具体的任务分发逻辑,将到期的 TimerQueueTimer 回调放入线程池执行。