揭秘 .NET 中 TimerQueue 的秘密(第 2 部分)
- 前言
- TimerQueue与OS定时器的交互
- 按需注册定时器
- AutoResetEvent封装了OS定时器
- 定时任务的管理
- 总结
前言
上面给大家介绍了TimerQueue的任务调度算法。
https://www.introzo.com/eventhorizon/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 方法的地方有两个
-
UpdateTimer方法,该方法用于注册或更新TimerQueueTimer。
-
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方法的实现。
需要注意以下三个方法
- SetTimer:用于注册定时器
- InitializeScheduledTimerManager_Locked:只会调用一次,用于初始化TimerQueue的定时器管理器,主要是初始化TimerThread。
- 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放入线程池中执行。
欢迎关注个人技术公众号
相关文章
- 10-05 ZEEKR 001 FR高性能车型预热:搭载4电机
- 10-05 我国网民规模达10.79亿,互联网普及率达76.4
- 10-05 《2023年标准地图》正式上线
- 10-05 stm32内存包括哪些类型(stm32内存分为几个
- 10-05 stm32采集+数据处理程序(stm32采集+数据
- 10-05 stm32仿真图的引脚如何连接(stm32仿真器接
- 10-05 Zabbix警告问题
- 10-05 Zabbix集成云预警(瑞祥云)实现电话短信预警
- 10-05 利用企业微信实现预警(shell+python)
- 10-05 高通CEO爆料苹果自研5G芯片明年准备就绪
- 10-05 全球智能手机市场被扰乱:iPhone与Androi
- 10-05 苹果最新巧克力广告:Apple Card激活到付款
- 10-05 苹果推出iOS 15.6正式版固件:我们来看看iO
- 10-05 iPadOS 16 允许应用程序使用 M1 设备存
- 10-05 为迎接Apple Watch 10周年:Appl
- 10-05 STM32连接esp32(stm32连接esp32
- 10-05 stm32串口dma发送和接收周期数据和随机数据(
- 10-05 stm32点亮led灯ad20 (stm32点亮l
- 10-05 esp32编程接线图(esp32编程程序接线图)
- 10-05 DAC0832波形发生器课程设计报告(dac083
- 最近发表