前言

上文给大家介绍了 TimerQueue 的任务调度算法。

https://www.cnblogs.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 的管理,而本文将基于. NET 7 版本的代码介绍 TimerQueue 是如何与 OS

的定时器进行交互的。

TimerQueue 与 OS 定时器的交互

按需注册定时器

TimerQueue 向 OS 注册定时器的过程被封装在 TimerQueueTimer 的 EnsureTimerFiresBy 方法中。

有两处地方会调用 EnsureTimerFiresBy 方法

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

  2. FireNextTimers 方法中,此方法用于遍历和执行 TimerQueue 中的 TimerQueueTimer。如果遍历完所有到期的 TimerQueueTimer 后,发现

    TimerQueue 中还有未到期的

    TimerQueueTimer,那么会调用 EnsureTimerFiresBy 方法,保证后面到期的 TimerQueueTimer 能够被及时执行。

internal class TimerQueue : IThreadPoolWorkItem
{
private bool _isTimerScheduled;
private long _currentTimerStartTicks;
private uint _currentTimerDuration; private bool EnsureTimerFiresBy(uint requestedDuration)
{
// TimerQueue 会将 requestedDuration 限制在 0x0fffffff 以内
// 0x0fffffff = 268435455 = 0x0fffffff / 1000 / 60 / 60 / 24 = 3.11 天
// 也就是说,runtime 会将 requestedDuration 限制在 3.11 天以内
// 因为 runtime 的定时器实现对于很长时间的定时器不太好用
// OS 的定时器可能会提前触发,但是这没关系,TimerQueue 会检查定时器是否到期,如果没有到期,TimerQueue 会重新注册定时器
const uint maxPossibleDuration = 0x0fffffff;
uint actualDuration = Math.Min(requestedDuration, maxPossibleDuration); if (_isTimerScheduled)
{
long elapsed = TickCount64 - _currentTimerStartTicks;
if (elapsed >= _currentTimerDuration)
return true; // 当前定时器已经到期,不需要重新注册定时器 uint remainingDuration = _currentTimerDuration - (uint)elapsed;
if (actualDuration >= remainingDuration)
return true; // 当前定时器的到期时间早于 requestedDuration,不需要重新注册定时器
} // 注册定时器
if (SetTimer(actualDuration))
{
_isTimerScheduled = true;
_currentTimerStartTicks = TickCount64;
_currentTimerDuration = actualDuration;
return true;
} return false;
}
}

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

AutoResetEvent 封装 OS 定时器

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

TimerQueue 使用了一个 AutoResetEvent 来等待定时器到期,封装了和 OS 定时器的交互。

AutoResetEvent 是一个线程同步的基元,它封装了一个内核对象,这个内核对象的状态有两种:终止状态和非终止状态,通过构造函数的

initialState 参数指定。

当调用 AutoResetEvent.WaitOne() 时,如果 AutoResetEvent 的状态为非终止状态,那么当前线程会被阻塞,直到 AutoResetEvent

的状态变为终止状态。

当调用 AutoResetEvent.Set() 时,如果 AutoResetEvent 的状态为非终止状态,那么 AutoResetEvent 的状态会变为终止状态,并且会唤醒一个等待的线程。

当调用 AutoResetEvent.Set() 时,如果 AutoResetEvent 的状态为终止状态,那么 AutoResetEvent 的状态不会发生变化,也不会唤醒等待的线程。

// 初始化为非终止状态,调用 WaitOne 会被阻塞
var autoResetEvent = new AutoResetEvent(initialState: false);
Task.Run(() =>
{
Console.WriteLine($"Task start {DateTime.Now:HH:mm:ss.fff}");
// 等待 Set 方法的调用,将 AutoResetEvent 的状态变为终止状态
autoResetEvent.WaitOne();
Console.WriteLine($"WaitOne1 end {DateTime.Now:HH:mm:ss.fff}");
// 每次被唤醒后,都会重新进入阻塞状态,等待下一次的唤醒
autoResetEvent.WaitOne();
Console.WriteLine($"WaitOne2 end {DateTime.Now:HH:mm:ss.fff}");
}); Thread.Sleep(1000);
autoResetEvent.Set();
Thread.Sleep(2000);
autoResetEvent.Set(); Console.ReadLine();

输出结果如下

Task start 10:42:39.914
WaitOne1 end 10:42:40.916
WaitOne2 end 10:42:42.918

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

public virtual bool WaitOne(TimeSpan timeout)
public virtual bool WaitOne(int millisecondsTimeout)
var autoResetEvent = new AutoResetEvent(false);
Task.Run(() =>
{
Console.WriteLine($"Task start {DateTime.Now:HH:mm:ss.fff}");
// 虽然 Set 方法在 2 秒后执行,但因为 WaitOne 方法的超时时间为 1 秒,所以 1 秒后就会执行下面的代码
autoResetEvent.WaitOne(TimeSpan.FromSeconds(1));
Console.WriteLine($"Task end {DateTime.Now:HH:mm:ss.fff}");
}); Thread.Sleep(2000);
autoResetEvent.Set(); Console.ReadLine();

输出结果如下

Task start 10:51:36.412
Task end 10:51:37.600

定时任务的管理

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

我们一共需要关注下面三个方法

  1. SetTimer:用于注册定时器
  2. InitializeScheduledTimerManager_Locked:只会被调用一次,用于初始化 TimerQueue 的定时器管理器,主要是初始化 TimerThread。
  3. TimerThread:用于处理 OS 定时器到期的线程,所有的 TimerQueue 共用一个 TimerThread。TimerThread 会在 OS 定时器到期时被唤醒,然后会遍历所有的 TimerQueue,找到到期的

    TimerQueue,然后将到期的 TimerQueue 放入到线程池中执行。
// TimerQueue 实现了 IThreadPoolWorkItem 接口,这意味着 TimerQueue 可以被放入到线程池中执行
internal class TimerQueue : IThreadPoolWorkItem
{
private static List<TimerQueue>? s_scheduledTimers;
private static List<TimerQueue>? s_scheduledTimersToFire; // TimerQueue 使用了一个 AutoResetEvent 来等待定时器到期,封装了和 OS 定时器的交互
// intialState = false,表示 AutoResetEvent 的初始状态为非终止状态
// 这样,当调用 AutoResetEvent.WaitOne() 时,因为 AutoResetEvent 的状态为非终止状态,那么调用线程会被阻塞
// 被阻塞的线程会在 AutoResetEvent.Set() 被调用时被唤醒
// AutoResetEvent 在被唤醒后,会将自己的状态设置为非终止状态,这样,下一次调用 AutoResetEvent.WaitOne() 时,调用线程会被阻塞
private static readonly AutoResetEvent s_timerEvent = new AutoResetEvent(false); private bool _isScheduled;
private long _scheduledDueTimeMs; private bool SetTimer(uint actualDuration)
{
long dueTimeMs = TickCount64 + (int)actualDuration;
AutoResetEvent timerEvent = s_timerEvent;
lock (timerEvent)
{
if (!_isScheduled)
{
List<TimerQueue> timers = s_scheduledTimers ?? InitializeScheduledTimerManager_Locked(); timers.Add(this);
_isScheduled = true;
} _scheduledDueTimeMs = dueTimeMs;
} // 调用 AutoResetEvent.Set(),唤醒 TimerThread
timerEvent.Set();
return true;
} private static List<TimerQueue> InitializeScheduledTimerManager_Locked()
{
var timers = new List<TimerQueue>(Instances.Length);
s_scheduledTimersToFire ??= new List<TimerQueue>(Instances.Length); Thread timerThread = new Thread(TimerThread)
{
Name = ".NET Timer",
IsBackground = true // 后台线程,当所有前台线程都结束时,后台线程会自动结束
};
// 使用 UnsafeStart 方法启动线程,是为了避免 ExecutionContext 的传播
timerThread.UnsafeStart(); // 这边是个设计上的细节,如果创建线程失败,那么会在下次创建线程时再次尝试
s_scheduledTimers = timers;
return timers;
} // 这个方法会在一个专用的线程上执行,它的作用是处理定时器请求,并在定时器到期时通知 TimerQueue
private static void TimerThread()
{
AutoResetEvent timerEvent = s_timerEvent;
List<TimerQueue> timersToFire = s_scheduledTimersToFire!;
List<TimerQueue> timers;
lock (timerEvent)
{
timers = s_scheduledTimers!;
} // 初始的Timeout.Infinite表示永不超时,也就是说,一开始只有等到 AutoResetEvent.Set() 被调用时,线程才会被唤醒
int shortestWaitDurationMs = Timeout.Infinite;
while (true)
{
// 等待定时器到期或者被唤醒
timerEvent.WaitOne(shortestWaitDurationMs); long currentTimeMs = TickCount64;
shortestWaitDurationMs = int.MaxValue;
lock (timerEvent)
{
// 遍历所有的 TimerQueue,找到到期的 TimerQueue
for (int i = timers.Count - 1; i >= 0; --i)
{
TimerQueue timer = timers[i];
long waitDurationMs = timer._scheduledDueTimeMs - currentTimeMs;
if (waitDurationMs <= 0)
{
timer._isScheduled = false;
timersToFire.Add(timer); int lastIndex = timers.Count - 1;
if (i != lastIndex)
{
timers[i] = timers[lastIndex];
} timers.RemoveAt(lastIndex);
continue;
} // 找到最短的等待时间
if (waitDurationMs < shortestWaitDurationMs)
{
shortestWaitDurationMs = (int)waitDurationMs;
}
}
} if (timersToFire.Count > 0)
{
foreach (TimerQueue timerToFire in timersToFire)
{
// 将到期的 TimerQueue 放入到线程池中执行
// UnsafeQueueHighPriorityWorkItemInternal 方法会将 timerToFire 放入到线程池的高优先级队列中,这个是 .NET 7 中新增的功能
ThreadPool.UnsafeQueueHighPriorityWorkItemInternal(timerToFire);
} timersToFire.Clear();
} if (shortestWaitDurationMs == int.MaxValue)
{
shortestWaitDurationMs = 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() 被调用或者OS定时器到期时被唤醒,然后遍历所有的 TimerQueue,找到到期的 TimerQueue,然后将到期的 TimerQueue 放入到线程池中执行。

TimerQueue 在被放入到线程池中执行时,会调用 FireNextTimers 方法,这个方法会遍历 TimerQueue 保存的 TimerQueueTimer,找到到期的 TimerQueueTimer,然后将到期的

TimerQueueTimer 放入到线程池中执行。

揭秘 .NET 中的 TimerQueue(下)的更多相关文章

  1. 揭秘JavaScript中谜一样的this

      揭秘JavaScript中谜一样的this 在这篇文章里我想阐明JavaScript中的this,希望对你理解this的工作机制有一些帮助.作为JavaScript程序员学习this对于你的发展有 ...

  2. Spark Streaming揭秘 Day31 集群模式下SparkStreaming日志分析(续)

    Spark Streaming揭秘 Day31 集群模式下SparkStreaming日志分析(续) 今天延续昨天的内容,主要对为什么一个处理会分解成多个Job执行进行解析. 让我们跟踪下Job调用过 ...

  3. Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析

    Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析 今天通过集群运行模式观察.研究和透彻的刨析SparkStreaming的日志和web监控台. Day28 ...

  4. Linux中/proc目录下文件详解

    转载于:http://blog.chinaunix.net/uid-10449864-id-2956854.html Linux中/proc目录下文件详解(一)/proc文件系统下的多种文件提供的系统 ...

  5. SQL搜索下划线,like中不能匹配下划线的问题

    最近在检测天气预报15天查询网 站(http://tqybw.net)时的URL时,发现页面中有很些404页,分析发现,是请求地址的能参数中多了下划线“_”,而rewrite规 则中并没有配这样的规则 ...

  6. python中那些双下划线开头得函数和变量--转载

    Python中下划线---完全解读     Python 用下划线作为变量前缀和后缀指定特殊变量 _xxx 不能用'from module import *'导入 __xxx__ 系统定义名字 __x ...

  7. struts2 jsp表单提交后保留表单中输入框中的值 下拉框select与input

    原文地址:struts2 jsp表单提交后保留表单中输入框中的值 下拉框select与input jsp页面 1     function dosearch() {2         if ($(&q ...

  8. Cocos开发中Visual Studio下HttpClient开发环境设置

    Cocos2d-x 3.x将与网络通信相关的类集成到libNetwork类库工程中,这其中包括了HttpClient类.我们需要在Visual Studio解决方案中添加libNetwork类库工程. ...

  9. Cocos开发中Visual Studio下libcurl库开发环境设置

    我们介绍一下win32中Visual Studio下libcurl库开发环境设置.Cocos2d-x引擎其实已经带有为Win32下访问libcurl库,Cocos2d-x 3.x中libcurl库文件 ...

  10. Android 获取SDCard中某个目录下图片

    本文介绍Android开发中如何获取SDCard中某目录下的所有图片并显示出来,下面的我们提供的这个函数是通用的,只要提供路径就可以查询出该目录下所有图片的路径信息,并保存到一个List<Str ...

随机推荐

  1. 【Redis】Setninel 哨兵机制

    一.Sentinel 哨兵工作原理 Redis在2.6+以后引入哨兵机制,在2.8版本后趋于稳定状态,在生产环境中建议使用2.8版本以上的sentinel服务.sentinel集群用于监控redis集 ...

  2. Python网页开发神器fac 0.2.8、fuc 0.1.28新版本更新内容介绍

    fac项目地址:https://github.com/CNFeffery/feffery-antd-components fuc项目地址:https://github.com/CNFeffery/fe ...

  3. 2023-03-25:若两个正整数的和为素数,则这两个正整数称之为“素数伴侣“。 给定N(偶数)个正整数中挑选出若干对,组成“素数伴侣“, 例如有4个正整数:2,5,6,13, 如果将5和6分为一组的

    2023-03-25:若两个正整数的和为素数,则这两个正整数称之为"素数伴侣". 给定N(偶数)个正整数中挑选出若干对,组成"素数伴侣", 例如有4个正整数:2 ...

  4. 2022-09-18:以下go语言代码输出什么?A:1;B:15;C:panic index out of range;D:doesn’t compile。 package main import

    2022-09-18:以下go语言代码输出什么?A:1:B:15:C:panic index out of range:D:doesn't compile. package main import ( ...

  5. 2021-08-09:给定一个有正、有负、有0的数组arr,给定一个整数k,返回arr的子集是否能累加出k。1)正常怎么做?2)如果arr中的数值很大,但是arr的长度不大,怎么做?

    2021-08-09:给定一个有正.有负.有0的数组arr,给定一个整数k,返回arr的子集是否能累加出k.1)正常怎么做?2)如果arr中的数值很大,但是arr的长度不大,怎么做? 福大大 答案20 ...

  6. 2021-09-06:给表达式添加运算符。给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target ,在 num 的数字之间添加 二元 运算符(不是一元)+、- 或 * ,返回所有

    2021-09-06:给表达式添加运算符.给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target ,在 num 的数字之间添加 二元 运算符(不是一元)+.- 或 * ,返回所有 ...

  7. Django接入drf_yasg2 API接口文档-完整操作(包含错误处理)

    drf_yasg2的简介: drf-yasg是Django RestFramework的一个扩展,使⽤drf_yasg2下载⾃动⽣成的api⽂档的json或yaml⽂件配置项. drf_yasg2的安 ...

  8. .NET周报 【5月第3期 2023-05-21】

    国内文章 C# 实现 Linux 视频会议(源码,支持信创环境,银河麒麟,统信UOS) https://www.cnblogs.com/shawshank/p/17390248.html 信创是现阶段 ...

  9. MVCC-数据库

    参考地址:看一遍就理解:MVCC原理详解 - 掘金 (juejin.cn) 1. 相关数据库知识点回顾 1.1 什么是数据库事务,为什么要有事务 事务,由一个有限的数据库操作序列构成,这些操作要么全部 ...

  10. hosts文件妙用,提升网站访问速度!

    一.背景 在讲解hosts文件之前,我们先了解下IP地址与域名的关系. 1.IP地址与域名的关系 IP(Internet Protocol)是一种规定互联网中数据传输的协议,每台连接到互联网中的计算机 ...