C# 计时器对象如何决定何时发生了经过的时间量?
我想知道它是否只是循环或比这更智能。
此外,最好知道在哪里可以找到 C# 源代码以进行查看。
CLR 创建一个专用线程来处理您在应用程序中创建的所有计时器对象,并使它们的 Elapsed 事件和回调处理程序运行。您可以看到它与调试器一起使用。从如下所示的控制台模式应用程序开始:
using System;
using System.Timers;
class Program {
static void Main(string[] args) {
var t = Timer();
t.Elapsed += ElapsedEventHandler((s, e) => { });
t.Start();
}
}
项目+属性,调试选项卡,勾选“启用本机代码调试”(又名非托管代码)选项。工具 + 选项、调试、符号并确保 Microsoft 符号服务器已启用。按 F11 开始调试。
现在使用调试 + Windows + 线程。您将看到列出的 4 个线程。您的主线程、终结器线程、调试线程和空闲线程池线程。继续前进,直到您通过 t.Start() 方法调用。请注意,现在添加了一个新线程。名称是“ThreadPoolMgr::TimerThreadStart”。双击它并查看调用堆栈窗口。你会看到的:
ntdll.dll!_NtDelayExecution@8() + 0x15 bytes
ntdll.dll!_NtDelayExecution@8() + 0x15 bytes
KernelBase.dll!_SleepEx@8() + 0x39 bytes
clr.dll!ThreadpoolMgr::TimerThreadFire() + 0x3e bytes
clr.dll!ThreadpoolMgr::TimerThreadStart() + 0x6a bytes
kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes
ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes
ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes
这里的重要位是 TimerThreadStart() 函数,它是 CLR 启动的线程的起点。而 SleepEx() 调用,就是这个线程在下一个计时器到期之前一直处于休眠状态。此堆栈跟踪适用于 .NET 4.5,自 .NET 2.0 以来一直如此。对于哪些源代码可用,您可以查看可在此处下载的 SSCLI20 源代码。搜索该代码会将您带到 clr/src/vm/win32threadpool.cpp 源代码文件。看看它,看看发生了什么。
我将简单地描述一下。计时源是 GetTickCount() api 函数,与 Environment.TickCount 使用的相同。FireTimers() 函数排序出哪个活动计时器首先到期。SleepEx() 函数与 Thread.Sleep() 函数相同,不同之处在于它是alertable。它可以在睡眠完成之前被 APC(异步过程调用)中断。您看到在同一个文件中使用 QueueUserAPC() 函数,当线程因为程序关闭而需要终止时,以及当添加或修改计时器以便需要计算新的睡眠时。
.NET BCL 至少有两个类提供计时器:
但是,第二个类是使用第一个类实现的。第一类使用某种本机 Win32 计时器,Hans Passant 提供了有关所使用机制的更多信息。在 BCL 源代码中,计时器是使用名为TimerQueue
. 该类的实现TimerQueue
有这样的评论:
TimerQueue 维护此 AppDomain 中的活动计时器列表。我们使用由 VM 提供的单个本机计时器来调度 AppDomain 中的所有托管计时器。
所以基本上 AppDomain 有一个使用非托管代码实现的单一时间源。有关更多信息,请参阅 Hans Passant 的回答。
此外,最好知道在哪里可以找到 C# 源代码以进行查看。
我使用JetBrains dotPeek。它可以配置为从 Microsoft 检索源文件(如果存在)。只需加载您想要检查的 .NET 版本,点击Ctrl+ T,键入“Timer”,选择您想要的特定类,如果存在,它将下载源代码。否则你会看到一个反编译的版本(取决于你的配置)。
请注意,此工具仅允许您对托管代码进行反编译并完全了解 .NET 中计时器的实现,您需要能够进一步了解非托管代码。