前两天刚好跟同学提起如何实现一个 Timer 。提到了 Kafka 的时间轮和 Go 语言的四叉堆实现。所以就看了下 .NET 是如何实现 Timer 的。
.NET Timer 分为两种,一种是 System.Windows.Threading.DispatcherTimer
, 另外一种是System.Timers.Timer
。
System.Windows.Threading.DispatcherTimer
.NET Framework 相关源码路径:
- System\Windows\Threading\DispatcherTimer.cs
- System\Windows\Threading\Dispatcher.cs
- System\Windows\Threading\DispatcherOperation.cs
简要实现原理:在每次新增 DispatcherTimer 的时候,都会将回调的委托存入 Dispatcher 中的 DispatcherOperation 优先队列里,但是优先级是最差的。然后将 Timer 本身存入当前 Dispatcher 的 Timer List 中。还有一个值得关注的是,时间间隔会加上系统运行时间 Environment.TickCount
,变成绝对时间保存下来,这是为了后边 WM_TIMER
到达之后,对比是否超时做准备。接下来就要关注 Dispatcher 了,当 Dispatcher 新增、删除、响应 Timer 事件以及 DispatcherTimer 调整时间间隔的时候,会调用 UpdateWin32Timer()
, 这个方法会在当前 Dispatcher 的 Timer List 中检索最近要触发的 DispatcherTimer,如果当前没有调用过 SetTimer()
或者调用过的 SetTimer
时间间隔比当前最近要触发的长,就取时间间隔,调用 SetTimer()
。当收到 WM_TIMER
消息之后,将根据程序运行时间,对比时间间隔,选出已经超时的 Timer,将之前提到的 DispatcherOperation 优先级提升,等到下一个消息循环来到时,回调 Operation 将会被从优先对列取出,并执行。
System.Timers.Timer
.NET Framework 相关源码路径:
- services\timers\system\timers\Timer.cs
- system\threading\timer.cs
- coreclr\src\vm\comthreadpool.cpp
简要实现原理:System.Timers.Timer
只是对 System.Threading.Timer
包装,所以实现上看 System.Threading.Timer
就好。这就不得不提到 System.Threading.Timer
中的 TimerQueue
。 这是存有 TimerQueueTimer
的双向队列。每增加一个 Timer 的时候,都会将一个 TimerQueueTimer
放入 TimerQueue
队列。同时调用运行时的 Native 的代码 AppDomainTimerNative::CreateAppDomainTimer()
。后边就是 Native 的代码逻辑了,具体细节不表了,简单理解就是在线程池中搞一个线程,在线程中调用 SleepEx()
阻塞线程,当线程走完之后触发回调,再调回 .NET 托管代码,找到 TimerQueueTimer
,再执行用户回调。
QTimer
相关源码路径:
- qtbase\src\corelib\kernel\qeventdispatcher_win.cpp
- qtbase\src\corelib\kernel\qtimer.cpp
- qtbase\src\corelib\kernel\qobject.cpp
QTimer
的实现就比较简单了,当增加一个 QTimer
的时候,会在 QEventDispatcher
中调用 Win32 API,同时在 QObject
中将 TimerId 保存到 Vector 中。唯一的细节是,时间间隔在 20ms 以下或者指定 QTimerType 为 Qt::PreciseTimer 的 QTimer
会在底层调用 timeSetEvent()
(源码注释中也提到了,虽然方法废弃了,但是精度还是高依旧使用),而其他的就调用 SetTimer()
方法。
谈谈 SetTimer
SetTimer()
的调用是有限制的。不管别人信不信,反正我是信了。这一点在 MSDN 中 SetTimer 的描述并没有,不过通过一些现象,以及网上的一些其他帖子可以得到认证。据 SO 上的一位吃瓜网友表示,SetTimer()
会创建用户对象(虽然这一点微软也没说过),而用户对象在系统中是有限制的(这一点是微软明确说过的),而用户对象的数量上限是在注册表中的,根据微软的文档指示应该是在: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\USERProcessHandleQuota
我看了一下 x64 系统应该是在 HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Windows\USERProcessHandleQuota
。默认数量是 10000 。
小结
分析过以上几种 Timer 的实现,就知道 .NET 的 Timer 还是做了一些微小的优化的。这也是为什么我跟同事说, 即使都是拿来做 Windows 桌面开发,.NET 框架的上限还是要比 Qt 高的原因。这大概是因为 .NET 本身从一开始就不是以桌面开发作为目标的,所以它更要考虑性能问题,但正因为如此,源码看起来比 Qt 就更为困难;而 Qt 这么实现,对一般的桌面应用来说,完全够用,代码也更容易看懂。虽然两者的实现在极端情况下都会拉闸,但是显然 Qt 的 Timer 实现会更快拉闸……