当前位置:首页 > 技术 > 正文内容

.NET Runtime 中 TimerQueue 与系统定时器的协作机制

访客 技术 2026年9月15日 11

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 回调放入线程池执行。

标签: .NET

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。