欢迎来到Introzo百科
Introzo百科
当前位置:网站首页 > 技术 > 揭秘 .NET 中 TimerQueue 的秘密(第 2 部分)

揭秘 .NET 中 TimerQueue 的秘密(第 2 部分)

日期:2023-10-05 13:43

内容
  • 前言
  • TimerQueue与OS定时器的交互
    • 按需注册定时器
    • AutoResetEvent封装了OS定时器
    • 定时任务的管理
  • 总结

前言

上面给大家介绍了TimerQueue的任务调度算法。
https://www.introzo.com/eventhorizo​​n/p/17557821.html

这是一个简单的回顾。

TimerQueue中的基本任务单元是TimerQueueTimer,它封装了要执行的定时任务。

TimeQueue根据任务过期时间分为shortTimer和longTimer两个队列,分别存储在TimerQueue的shortTimers和longTimers两个双向链表中。

Runtime根据CPU核数创建相同数量的TimerQueue。每个TimerQueueTimer都会根据创建的CPU核分配到对应的TimerQueue,并根据任务过期时间插入到对应的shortTimer或longTimer队列中。中间。

每个TimerQueue都会根据其管理的TimerQueueTimer的过期时间维护一个最小过期时间。这个最小过期时间就是TimerQueue自己的过期时间。 TimerQueue 将向操作系统注册自己的到期时间。 (以下简称OS)定时器。

当OS定时器到期时,TimerQueue会收到通知,TimerQueue会将到期的TimerQueueTimer从shortTimer或longTimer队列中移除,并将定时任务放入线程池中执行。

上面主要介绍了TimerQueue对TimerQueueTimer的管理,本文将介绍TimerQueue如何基于. NET 7 版本的代码。

TimerQueue 与 OS 定时器的交互

按需注册定时器

TimerQueue 向操作系统注册定时器的过程封装在TimerQueueTimer的EnsureTimerFiresBy方法中。调用 EnsureTimerFiresBy 方法的地方有两个

  1. UpdateTimer方法,该方法用于注册或更新TimerQueueTimer。

  2. FireNextTimers方法,该方法用于遍历并执行TimerQueue中的TimerQueueTimer。如果遍历完所有过期的TimerQueueTimer后,我们发现
    TimerQueue 中还有未过期的
    TimerQueueTimer,那么会调用EnsureTimerFiresBy方法,保证后面过期的TimerQueueTimer能够及时执行。

内部类 TimerQueue : IThreadPoolWorkItem
{
    私人布尔_isTimerScheduled;
    私有长_currentTimerStartTicks;
    私有uint _currentTimerDuration;
    
    私人 bool EnsureTimerFiresBy(uint requestDuration)
    {
        // TimerQueue 会将 requestDuration 限制在 0x0fffffff 以内
        // 0x0fffffff = 268435455 = 0x0fffffff / 1000 / 60 / 60 / 24 = 3.11 天
        // 换句话说,运行时会将requestedDuration限制为3.11天
        // 因为运行时定时器实现对于很长的定时器来说并不容易使用。// 操作系统定时器可能会提前触发,但这并不重要。 TimerQueue会检查定时器是否过期。如果没有过期,TimerQueue会重新注册定时器。
        const uint maxPossibleDuration = 0x0ffffffff;
        uint实际持续时间 = Math.Min(requestedDuration, maxPossibleDuration);

        如果(_isTimerScheduled)
        {
            经过的时间= TickCount64 - _currentTimerStartTicks;
            if (已过去 >= _currentTimerDuration)
                返回真; // 当前定时器已过期,无需重新注册定时器

            uint剩余持续时间 = _currentTimerDuration - (uint)已过去;
            if (实际持续时间 >= 剩余持续时间)
                返回真; // 当前定时器的过期时间早于requestedDuration,不需要重新注册定时器
        }

        //注册定时器
        if (SetTimer(实际持续时间))
        {
            _isTimerScheduled = true;
            _currentTimerStartTicks = TickCount64;
            _currentTimerDuration = 实际持续时间;
            返回真;
        }

        返回假;
    }
}

EnsureTimerFiresBy方法中,会记录当前TimerQueue的过期时间和状态,并根据需要判断定时器是否需要重新注册。

AutoResetEvent 封装了操作系统定时器

在进一步介绍TimerQueue如何与OS定时器交互之前,我们先来看看AutoResetEvent。

TimerQueue 使用 AutoResetEvent 等待定时器到期,封装了与操作系统定时器的交互。

AutoResetEvent 是封装内核对象的线程同步原语。这个内核对象有两种状态:终止状态和非终止状态,由构造函数的initialState参数指定。

调用 AutoResetEvent.WaitOne() 时,如果 AutoResetEvent 的状态为非终止,则当前线程将被阻塞,直到 AutoResetEvent 的状态变为终止。

调用 AutoResetEvent.Set() 时,如果 AutoResetEvent 的状态为非终止,则 AutoResetEvent 的状态将更改为终止状态,并唤醒等待的线程。

当调用AutoResetEvent.Set()时,如果AutoResetEvent的状态被终止,则AutoResetEvent的状态不会改变,等待的线程也不会被唤醒。

//初始化为非终止状态,调用WaitOne会被阻塞
var autoResetEvent = new AutoResetEvent(initialState: false);
任务.运行(() =>
{
    Console.WriteLine($"任务开始{www.introzo.com:HH:mm:ss.fff}");
    // 等待Set方法被调用并将AutoResetEvent的状态更改为终止状态
    autoResetEvent.WaitOne();Console.WriteLine($"WaitOne1 end {www.introzo.com:HH:mm:ss.fff}");
    // 每次被唤醒,都会重新进入阻塞状态,等待下一次唤醒。
    autoResetEvent.WaitOne();
    Console.WriteLine($"WaitOne2 end {www.introzo.com:HH:mm:ss.fff}");
});

线程.睡眠(1000);
autoResetEvent.Set();
线程睡眠(2000);
autoResetEvent.Set();

Console.ReadLine();

输出结果如下

任务开始10:42:39.914
WaitOne1 结束 10:42:40.916
WaitOne2 结束 10:42:42.918

同时,AutoResetEvent还提供了WaitOne方法的重载,可以指定等待时间。如果AutoResetEvent的状态在指定时间内没有变为终止状态,则WaitOne停止等待并唤醒线程。

public virtual bool WaitOne(TimeSpan 超时)
公共虚拟布尔WaitOne(int毫秒超时)
var autoResetEvent = new AutoResetEvent(false);
任务.运行(() =>
{
    Console.WriteLine($"任务开始{www.introzo.com:HH:mm:ss.fff}");
    //虽然Set方法是2秒后执行的,但由于WaitOne方法的超时时间是1秒,所以下面的代码会在1秒后执行
    autoResetEvent.WaitOne(TimeSpan.FromSeconds(1));
    Console.WriteLine($"任务结束{www.introzo.com:HH:mm:ss.fff}");
});线程睡眠(2000);
autoResetEvent.Set();

Console.ReadLine();

输出结果如下

任务开始10:51:36.412
任务结束 10:51:37.600

计划任务管理

接下来我们看一下SetTimer方法的实现。

需要注意以下三个方法

  1. SetTimer:用于注册定时器
  2. InitializeScheduledTimerManager_Locked:只会调用一次,用于初始化TimerQueue的定时器管理器,主要是初始化TimerThread。
  3. TimerThread:用于处理操作系统计时器到期的线程。所有TimerQueue共享一个TimerThread。当OS定时器到期时,TimerThread会被唤醒,然后它会遍历所有的TimerQueue,找到过期的TimerQueue,然后将过期的TimerQueue放入线程池中执行。
//TimerQueue实现了IThreadPoolWorkItem接口,也就是说TimerQueue可以放入线程池中执行
内部类 TimerQueue : IThreadPoolWorkItem
{
    私人静态列表? s_scheduledTimers;
    私人静态列表? s_scheduledTimersToFire;

    // TimerQueue 使用 AutoResetEvent 等待定时器到期,封装了与 OS 定时器的交互
    //initialState = false,表示AutoResetEvent的初始状态为非终止状态
    // 这样,当调用AutoResetEvent.WaitOne()时,由于AutoResetEvent的状态为非终止,所以调用线程会被阻塞// 调用 AutoResetEvent.Set() 时,阻塞的线程将被唤醒
    // AutoResetEvent被唤醒后,会将自身状态设置为非终止状态,这样下次调用AutoResetEvent.WaitOne()时调用线程就会被阻塞。
    私有静态只读 AutoResetEvent s_timerEvent = new AutoResetEvent(false);

    私有布尔_isScheduled;
    私人长_scheduledDueTimeMs;

    私人布尔SetTimer(uint实际持续时间)
    {
        长 dueTimeMs = TickCount64 + (int)actualDuration;
        自动重置事件timerEvent = s_timerEvent;
        锁定(计时器事件)
        {
            if (!_isScheduled)
            {
                列表计时器= s_scheduledTimers ??初始化ScheduledTimerManager_Locked();

                计时器.Add(this);
                _isScheduled = true;
            }

            _scheduledDueTimeMs = dueTimeMs;
        }

        // 调用AutoResetEvent.Set()唤醒TimerThread
        定时器事件.Set();
        返回真;
    }

    私有静态列表InitializeScheduledTimerManager_Locked()
    {vartimers = new List(Instances.Length);
        s_scheduledTimersToFire ??= new List(Instances.Length);

        线程timerThread = new Thread(TimerThread)
        {
            名称 = ".NET 计时器",
            isBackground = true //后台线程,当所有前台线程结束时,后台线程会自动结束
        };
        // 使用UnsafeStart方法启动线程以避免ExecutionContext的传播
        TimerThread.UnsafeStart();

        // 这是一个设计细节。如果线程创建失败,下次创建线程时会重试。
        s_scheduledTimers = 计时器;
        返回计时器;
    }

    // 该方法将在专用线程上执行。它的作用是处理定时器请求,并在定时器到期时通知TimerQueue。
    私有静态无效TimerThread()
    {
        自动重置事件timerEvent = s_timerEvent;
        列表timersToFire = s_scheduledTimersToFire!;
        列出计时器;
        锁定(计时器事件)
        {
            定时器= s_scheduledTimers!;
        }

        // 初始的Timeout.Infinite表示永远不会超时,也就是说,直到一开始调用AutoResetEvent.Set(),线程才会被唤醒。int ShortestWaitDurationMs = Timeout.Infinite;
        而(真)
        {
            // 等待定时器到期或者被唤醒
            timerEvent.WaitOne(shortestWaitDurationMs);

            长 currentTimeMs = TickCount64;
            最短等待时间 = int.MaxValue;
            锁定(计时器事件)
            {
                // 遍历所有TimerQueue,找到过期的TimerQueue
                for (int i =timers.Count - 1; i >= 0; --i)
                {
                    TimerQueue 计时器 = 计时器[i];
                    长waitDurationMs =计时器._scheduledDueTimeMs - currentTimeMs;
                    if (waitDurationMs <= 0)
                    {
                        计时器._isScheduled = false;
                        定时器ToFire.Add(定时器);

                        int lastIndex =timers.Count - 1;
                        if (i != 最后索引)
                        {
                            定时器[i] = 定时器[lastIndex];
                        }计时器.RemoveAt(lastIndex);
                        继续;
                    }
                    
                    // 寻找最短的等待时间
                    if (waitDurationMs < 最短WaitDurationMs)
                    {
                        最短等待时间 = (int)等待时间;
                    }
                }
            }

            if (timersToFire.Count > 0)
            {
                foreach(timerQueue中的timerToFire在timersToFire中)
                {
                    // 将过期的TimerQueue放入线程池中执行
                    // UnsafeQueueHighPriorityWorkItemInternal方法会将timerToFire放入线程池的高优先级队列中。这是.NET 7 中的新功能
                    ThreadPool.UnsafeQueueHighPriorityWorkItemInternal(timerToFire);
                }

                计时器ToFire.Clear();
            }

            if (shortestWaitDurationMs == int.MaxValue)
            {
                最短WaitDurationMs = Timeout.Infinite;}
        }
    }

    void IThreadPoolWorkItem.Execute() => FireNextTimers();
}

所有TimerQueue共享一个AutoResetEvent和一个TimerThread。当调用AutoResetEvent.Set()或者OS定时器过期时,TimerThread会被唤醒,然后TimerThread会遍历所有的TimerQueue,找到过期的TimerQueue,然后将过期的TimerQueue放入线程池中执行。

这样就实现了TimerQueue的定时器管理器。

总结

TimerQueue的实现是一个套娃过程。

TimerQueue 使用一个 AutoResetEvent 来等待定时器到期,封装了与 OS 定时器的交互,然后 TimerQueue 实现了 IThreadPoolWorkItem 接口,这意味着 TimerQueue 可以放入线程池中执行。

TimerQueue 的定时器管理器是一个专用线程。它等待 AutoResetEvent.Set() 被调用或在操作系统定时器到期时被唤醒。然后它会遍历所有的TimerQueue,找到过期的TimerQueue,然后将达到周期的TimerQueue放入线程池中执行。

当TimerQueue放入线程池执行时,会调用FireNextTimers方法。该方法会遍历TimerQueue保存的TimerQueueTimer,找到过期的TimerQueueTimer,然后将过期的TimerQueueTimer放入线程池中执行。

欢迎关注个人技术公众号

关灯