文章目录
-
- 1.System.Threading.Timer(线程计时器)
-
- [1.1. 方法签名](#1.1. 方法签名)
- [1.2. 参数说明](#1.2. 参数说明)
- [1.3. 特殊值处理](#1.3. 特殊值处理)
- [1.4. 示例:动态控制计时器](#1.4. 示例:动态控制计时器)
- 2.System.Timers.Timer(服务器计时器)
-
- [2.1. 示例:定期检查服务状态](#2.1. 示例:定期检查服务状态)
- 3.System.Windows.Forms.Timer(Windows计时器)
-
- [3.1. 示例:更新 WinForms 界面上的时间标签](#3.1. 示例:更新 WinForms 界面上的时间标签)
- System.Windows.Threading.DispatcherTimer
-
- [4.1. 示例:更新 WPF 界面上的数据显示](#4.1. 示例:更新 WPF 界面上的数据显示)
- 5.计时器核心区别:线程模型与精度
-
- [5.1. 线程模型](#5.1. 线程模型)
- [5.2. 计时精度](#5.2. 计时精度)
- [1. 后台计时器:`System.Threading.Timer` (线程池,高精度)](#1. 后台计时器:
System.Threading.Timer(线程池,高精度)) - [2. 后台计时器:`System.Timers.Timer` (事件驱动,线程池)](#2. 后台计时器:
System.Timers.Timer(事件驱动,线程池)) - [3. UI 计时器:`System.Windows.Forms.Timer` (WinForms,消息循环)](#3. UI 计时器:
System.Windows.Forms.Timer(WinForms,消息循环)) - [4. UI 计时器:`System.Windows.Threading.DispatcherTimer` (WPF,调度器)](#4. UI 计时器:
System.Windows.Threading.DispatcherTimer(WPF,调度器)) - [5.3. 如何进行跨线程操作 (后台 -> UI)](#5.3. 如何进行跨线程操作 (后台 -> UI))
- 6.Stopwatch类 (System.Diagnostics.Stopwatch)
-
- 核心用途
- 关键特点
- 主要方法和属性
- 示例:测量代码块耗时
- [Stopwatch vs. Timer](#Stopwatch vs. Timer)
- 7.精确计时器
-
- [7.1. 调用WIN API中的GetTickCount](#7.1. 调用WIN API中的GetTickCount)
- 介绍
- 优点与缺点
- 总结
- [7.2. timeGetTime (WIN API - 可调精度计时)](#7.2. timeGetTime (WIN API - 可调精度计时))
- 介绍
- 优点与缺点
- 总结
- [7.3. System.Environment.TickCount (.NET 封装 - 粗略计时)](#7.3. System.Environment.TickCount (.NET 封装 - 粗略计时))
- 介绍
- 优点与缺点
- 总结
- [7.4. `QueryPerformanceCounter` (Windows API - 高分辨率计数器)](#7.4.
QueryPerformanceCounter(Windows API - 高分辨率计数器)) - 介绍
- 优点与缺点
- 总结
- [7.5. `System.Diagnostics.Stopwatch` (.NET 推荐 - 高精度封装)](#7.5.
System.Diagnostics.Stopwatch(.NET 推荐 - 高精度封装)) - 介绍
- 优点与缺点
- 总结
- [7.6. CPU 时间戳 (Read Time-Stamp Counter, RDTSC)](#7.6. CPU 时间戳 (Read Time-Stamp Counter, RDTSC))
- 介绍
- 优点与缺点
- 总结
- 最终结论和对比
1.System.Threading.Timer(线程计时器)
1、最底层、轻量级的计时器。基于线程池实现的,工作在辅助线程。
2、它并不是内在线程安全的,并且使用起来比其他计时器更麻烦。此计时器通常不适合 Windows 窗体环境。
csharp
// ctor
public Timer(TimerCallback callback, object state, int dueTime, int period);
- 线程模型: 在 线程池线程 上执行回调方法(
TimerCallback委托)。 - 特点: 这是一个轻量级的计时器,设计用于在多线程环境或服务器环境(如服务、控制台应用)中进行高精度 、非 UI 的定期操作。
- 执行方式: 它不涉及事件,而是直接调用一个
TimerCallback委托。回调方法的执行会占用一个线程池线程。 - 注意: 由于回调方法在后台线程执行,它不能直接操作 UI 元素,否则会导致跨线程异常。
后台任务调度
csharp
using System;
using System.Threading;
public class ThreadingTimerExample
{
private static Timer _timer;
public static void Run()
{
Console.WriteLine($"主线程启动于: {DateTime.Now:HH:mm:ss.fff}");
// 创建 Timer:
// 1. TimerCallback 方法
// 2. 传递给回调方法的 object 状态对象(这里传递 null)
// 3. 第一次调用前的延迟时间 (dueTime):1000 毫秒
// 4. 后续调用之间的间隔时间 (period):2000 毫秒
_timer = new Timer(
TimerTask,
null,
1000,
2000
);
// 保持主程序运行,以便 Timer 可以继续执行
Console.WriteLine("按 Enter 键退出程序...");
Console.ReadLine();
// 停止并释放计时器
_timer.Dispose();
Console.WriteLine("计时器已停止。");
}
// TimerCallback 方法,在线程池线程上执行
private static void TimerTask(object state)
{
Console.WriteLine($"Timer 任务执行于: {DateTime.Now:HH:mm:ss.fff},线程 ID: {Thread.CurrentThread.ManagedThreadId}");
// 可以在这里执行短时间的后台任务
}
}
1.1. 方法签名
csharp
public bool Change(int dueTime, int period);
// 或者使用 TimeSpan 版本,更精确、更安全:
public bool Change(TimeSpan dueTime, TimeSpan period);
1.2. 参数说明
| 参数 | 类型 | 含义 |
|---|---|---|
| dueTime | int (毫秒) 或 TimeSpan | 第一次调用回调方法前的延迟时间。 |
| period | int (毫秒) 或 TimeSpan | 后续调用回调方法之间的间隔时间。 |
1.3. 特殊值处理
dueTime 和 period 都可以接受特殊值,用于控制计时器的行为:
| 值 | 含义 | 效果 |
|---|---|---|
| 0 | 零(0) | 表示立即调用回调方法。 |
| -1 | 负一(-1) | dueTime 为 -1 时,表示计时器已禁用,不会触发回调方法。 |
| -1 | 负一(-1) | period 为 -1 时,表示计时器只触发一次,之后自动禁用(单次模式)。 |
| Timeout.Infinite | 常量 (-1) | 与使用 -1 效果相同,是更清晰的表达方式。 |
1.4. 示例:动态控制计时器
下面的例子演示了如何使用 Change 方法:首先让计时器以 2 秒的间隔运行几次,然后将它切换到 5 秒的间隔,最后将它停止。
csharp
using System;
using System.Threading;
public class ThreadingTimerChangeExample
{
private static Timer _timer;
private static int _counter = 0;
public static void Run()
{
Console.WriteLine($"计时器启动。初始间隔:2秒。");
// 初始设置:1秒后第一次执行,之后每隔 2000 毫秒执行一次
_timer = new Timer(
TimerTask,
null,
1000,
2000
);
// 保持程序运行
Console.WriteLine("按 Enter 键观察变化和停止...");
Console.ReadLine();
// 确保计时器停止并释放资源
_timer.Dispose();
Console.WriteLine("程序退出。");
}
private static void TimerTask(object state)
{
_counter++;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 任务执行 {_counter} 次。 线程ID: {Thread.CurrentThread.ManagedThreadId}");
if (_counter == 3)
{
// 达到 3 次后,我们使用 Change 方法改变计时器的间隔
// 新设置:5000 毫秒(5秒)后第一次执行,之后每隔 5000 毫秒执行一次
_timer.Change(5000, 5000);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("--- 计时器间隔已修改为 5 秒 ---");
Console.ResetColor();
}
if (_counter == 5)
{
// 达到 5 次后,我们使用 Change 方法停止计时器
// 设置 dueTime 和 period 都为 -1,停止触发
_timer.Change(Timeout.Infinite, Timeout.Infinite);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("--- 计时器已停止 (通过 Change 方法) ---");
Console.ResetColor();
}
}
}
// 运行输出示例 (时间戳会有变化):
// 计时器启动。初始间隔:2秒。
// 按 Enter 键观察变化和停止...
// [10:00:01] 任务执行 1 次。 线程ID: 7
// [10:00:03] 任务执行 2 次。 线程ID: 8
// [10:00:05] 任务执行 3 次。 线程ID: 7
// --- 计时器间隔已修改为 5 秒 ---
// [10:00:10] 任务执行 4 次。 线程ID: 8 // 等待了 5 秒
// [10:00:15] 任务执行 5 次。 线程ID: 7 // 又等待了 5 秒
// --- 计时器已停止 (通过 Change 方法) ---
使用 Change 方法是控制 System.Threading.Timer 行为的标准和推荐方式。
2.System.Timers.Timer(服务器计时器)
- 针对服务器的服务程序,基于System.Threading.Timer,被设计并优化成能用于多线程环境 。在这种情况下,应该确保事件处理程序不与 UI 交互。在asp.net中一般使用System.Timers.Timer。
- 继承自Compnent,公开了可以SynchronizingObject 属性,避免了线程池中无法访问主线程中组件的问题(模拟System.Windows.Forms.Timer单线程模式)。但是除非需要对事件的时间安排进行更精确的控制,否则还是应该改为使用 System.Windows.Forms.Timer。
- AutoReset属性设置计时器是否在引发Elapsed事件后重新计时,默认为true。如果该属性设为False,则只执行timer_Elapsed方法一次。
- System.Timers.Timer是多线程定时器,如果一个Timer没有处理完成,到达下一个时间点,新的Timer同样会被启动。所以,Timer比较适合执行不太耗时的小任务,若在Timer中运行耗时任务,很容易出现由于超时导致的多线程重入问题,即多个线程同时进入timer_Elapsed方法。
- 为了应对多线程重入问题。可以加锁,也可以增加标志位。 Interlocked.Exchange提供了一种轻量级的线程安全的给对象赋值的方法,所以使用Interlocked.Exchange给变量赋值。
- 线程模型: 默认情况下,在 线程池线程 上引发
Elapsed事件。 - 特点: 这是一个事件驱动 的计时器,设计用于多线程环境或服务器应用,例如服务或控制台应用。它比
System.Threading.Timer更易于使用,因为它使用标准的 .NET 事件模型。 - 执行方式: 当间隔时间到达时,它会引发
Elapsed事件,该事件的处理程序默认在线程池线程上执行。它有一个AutoReset属性,可以控制事件是否重复触发。 - 注意: 与
System.Threading.Timer类似,由于事件处理程序在后台线程执行,它不能直接操作 UI 元素。
2.1. 示例:定期检查服务状态
csharp
using System;
using System.Timers;
public class TimersTimerExample
{
private static Timer _aTimer;
public static void Run()
{
// 实例化 Timer,并设置间隔为 3000 毫秒 (3 秒)
_aTimer = new System.Timers.Timer(3000);
// 关联 Elapsed 事件的处理方法
_aTimer.Elapsed += OnTimedEvent;
// 设置为重复引发事件 (默认为 true)
_aTimer.AutoReset = true;
// 启动计时器
_aTimer.Enabled = true;
Console.WriteLine("服务器计时器已启动。按 Enter 键退出...");
Console.ReadLine();
// 停止并释放计时器
_aTimer.Enabled = false;
_aTimer.Dispose();
Console.WriteLine("计时器已停止。");
}
private static void OnTimedEvent(Object source, ElapsedEventArgs e)
{
// 此代码在线程池线程上执行
Console.WriteLine($"检查系统状态于: {e.SignalTime:HH:mm:ss.fff}");
// 模拟执行一些检查操作,比如检查数据库连接、日志记录等。
}
}
3.System.Windows.Forms.Timer(Windows计时器)
此计时器直接继承自Component,它经过了专门的优化以便与 Windows 窗体一起使用,并且必须在窗口中使用。
- Windows计时器建立在基于消息的UI线程上运行,精度限定为5ms。Tick事件中执行的事件与主窗体是同一个线程(单线程),并且对与 UI 交互是安全的。
- 只有Enable和Internal两个属性和一个Tick事件,可以使用Start()和Stop()方法控制Enable属性。
- 线程模型: 仅在 UI 线程上执行,且需要一个活动的 UI 消息循环(即 WinForms 应用)。
- 特点: 专为 Windows Forms 应用程序设计。它的计时精度较低(受限于 WinForms 消息循环的频率),但最大的优点是它安全地访问 UI 元素 ,因为它的
Tick事件始终在创建它的 UI 线程上触发。 - 执行方式: 当间隔时间到达时,它会在 UI 线程的消息队列中放置一个消息,导致
Tick事件被触发。
3.1. 示例:更新 WinForms 界面上的时间标签
假设你在一个 WinForms 应用中有一个 Label 控件,名为 timeLabel。
csharp
// 这是一个 WinForms 窗体类中的代码片段
// 声明 Timer 变量
private System.Windows.Forms.Timer _formsTimer;
private void Form1_Load(object sender, EventArgs e)
{
_formsTimer = new System.Windows.Forms.Timer();
// 设置间隔为 1000 毫秒 (1 秒)
_formsTimer.Interval = 1000;
// 关联 Tick 事件的处理方法
_formsTimer.Tick += FormsTimer_Tick;
// 启动计时器
_formsTimer.Start();
}
private void FormsTimer_Tick(object sender, EventArgs e)
{
// 此代码在 UI 线程上执行,可以直接更新 UI 元素
timeLabel.Text = DateTime.Now.ToLongTimeString();
}
// 停止时,可以调用 _formsTimer.Stop();
System.Windows.Threading.DispatcherTimer
主要用于WPF中。属性和方法与System.Windows.Forms.Timer类似。DispatcherTimer中Tick事件执行是在主线程中进行的。
使用DispatcherTimer时有一点需要注意,因为DispatcherTimer的Tick事件是排在Dispatcher队列中的,当系统在高负荷时,不能保证在Interval时间段执行,可能会有轻微的延迟,但是绝对可以保证Tick的执行不会早于Interval设置的时间。如果对Tick执行时间准确性高可以设置DispatcherTimer的priority。
- 线程模型: 仅在与关联的 Dispatcher 线程 上执行(通常是 WPF/UWP/WinUI 的 UI 线程)。
- 特点: 专为 WPF 应用程序设计。它与
System.Windows.Forms.Timer的目的类似------安全地与 UI 交互,但它与 WPF 的Dispatcher机制集成,可以指定执行的优先级 。它的精度通常优于System.Windows.Forms.Timer。 - 执行方式: 当间隔时间到达时,它将一个操作加入到 Dispatcher 队列中,然后操作在 UI 线程上以指定的优先级执行,触发
Tick事件。
4.1. 示例:更新 WPF 界面上的数据显示
假设你在一个 WPF 窗口中有一个 TextBlock 控件,名为 statusText。
csharp
using System;
using System.Windows.Threading;
using System.Windows.Controls; // 用于 TextBlock
// 这是一个 WPF 窗口类中的代码片段
// 声明 DispatcherTimer 变量
private DispatcherTimer _dispatcherTimer;
private TextBlock statusText; // 假设这是 UI 上的一个元素
public MainWindow()
{
InitializeComponent();
// 假设 statusText 已在 XAML 中定义并初始化
_dispatcherTimer = new DispatcherTimer();
// 设置间隔为 500 毫秒 (0.5 秒)
_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(500);
// 关联 Tick 事件的处理方法
_dispatcherTimer.Tick += DispatcherTimer_Tick;
// 启动计时器
_dispatcherTimer.Start();
}
private void DispatcherTimer_Tick(object sender, EventArgs e)
{
// 此代码在 UI 线程上执行,可以直接更新 UI 元素
statusText.Text = $"当前时间: {DateTime.Now:HH:mm:ss.fff}\nUI 刷新中...";
}
// 停止时,可以调用 _dispatcherTimer.Stop();
总结
- 非 UI 任务(服务、控制台应用、后台工作):
- 高精度、轻量级、直接回调: 选择
System.Threading.Timer。 - 事件驱动、易于使用、需要
AutoReset: 选择System.Timers.Timer。
- 高精度、轻量级、直接回调: 选择
- Windows UI 应用中的 UI 更新:
- WinForms 应用: 选择
System.Windows.Forms.Timer。 - WPF/UWP/WinUI 应用: 选择
System.Windows.Threading.DispatcherTimer。
- WinForms 应用: 选择
| 计时器类 | 命名空间 | 适用环境 | 线程模型 | 精度/特点 | 典型用途 |
|---|---|---|---|---|---|
| System.Threading.Timer | System.Threading | 服务、控制台应用、多线程环境 | 线程池线程 (后台线程) | 高精度,适用于短时间的非 UI 操作 | 后台任务、调度、轮询 |
| System.Timers.Timer | System.Timers | 服务、控制台应用、多线程环境 | 线程池线程 (后台线程) | 高精度,事件驱动,具有 AutoReset 属性 | 定期检查、服务任务、触发事件 |
| System.Windows.Forms.Timer | System.Windows.Forms | Windows Forms (WinForms) UI 应用 | UI 线程 (单线程) | 精度较低(受 UI 消息循环限制),安全访问 UI | UI 更新、动画、状态栏刷新 |
| System.Windows.Threading.DispatcherTimer | System.Windows.Threading | Windows Presentation Foundation (WPF) UI 应用 | UI 线程 (单线程) | 精度较高(受 Dispatcher 队列限制),安全访问 UI | UI 更新、动画、实时数据显示 |
5.计时器核心区别:线程模型与精度
5.1. 线程模型
这是四种计时器最根本的区别,直接决定了它们的应用场景(UI vs. 后台)。
| 计时器类 | 运行线程 | 核心机制 | UI 访问安全性 |
|---|---|---|---|
| System.Threading.Timer | 线程池线程 | 采用 TimerCallback 委托,由 CLR 线程池调度执行。 | 不安全。必须手动使用 Invoke/Dispatcher 机制切换到 UI 线程。 |
| System.Timers.Timer | 线程池线程 | 引发 Elapsed 事件,默认由 CLR 线程池线程处理。 | 不安全。原因同上。 |
| System.Windows.Forms.Timer | UI 线程 | 依赖 WinForms 的消息循环(Message Loop)。它向消息队列发送一个计时消息。 | 安全。Tick 事件总是在创建它的 UI 线程上执行。 |
| System.Windows.Threading.DispatcherTimer | UI 线程 | 依赖 WPF 的 Dispatcher 队列。它将操作加入到 Dispatcher 队列中执行。 | 安全。Tick 事件总是在关联的 Dispatcher 线程上执行。 |
总结线程模型:
- 后台型 (Threading & Timers): 运行在线程池上,适用于高并发、服务器或后台任务。
- UI 型 (Forms & Dispatcher): 运行在单个 UI 线程上,适用于需要频繁更新界面的场景。
5.2. 计时精度
计时器的"精度"指的是它触发事件或回调的时间偏差(即,实际触发时间与设定时间之间的差异)。
| 计时器类 | 精度特点 | 性能开销 | 适用场景 |
|---|---|---|---|
| System.Threading.Timer | 最高 (基于内核 I/O 完成端口),高效率。 | 最轻量级。只涉及一个回调方法。 | 对时间要求严格的后台调度。 |
| System.Timers.Timer | 较高(基于内核),但略低于 Threading.Timer。 | 较高。涉及事件封装和锁定机制。 | 后台调度,但更喜欢事件模型。 |
| System.Windows.Forms.Timer | 最低,受限于 UI 消息循环频率。 | 轻量级,但效率受 UI 忙碌程度影响。 | 只需要大约每秒几次的简单 UI 更新。 |
| System.Windows.Threading.DispatcherTimer | 较高,受限于 Dispatcher 队列。 | 中等。可以设置优先级,影响精度。 | 需要更精确的 WPF 动画或实时数据更新。 |
关键点: UI 计时器 (Forms.Timer, DispatcherTimer) 的精度永远受限于 UI 线程的繁忙程度。如果 UI 线程正在执行耗时的操作(例如,一个长时间的按钮点击处理),计时器事件就会被延迟。后台计时器则没有这个问题。
1. 后台计时器:System.Threading.Timer (线程池,高精度)
- 优点: 精度高,开销低,最适合在无 UI 环境中或作为复杂多线程应用的一部分使用。
- 缺点: 无法直接访问 UI。如果回调方法执行时间过长,会阻塞线程池中的其他任务。
- 示例应用: 数据库连接池的超时检测、短时间的后台数据同步、自定义调度器。
2. 后台计时器:System.Timers.Timer (事件驱动,线程池)
- 优点: 使用标准的
Elapsed事件,编码更自然、更方便。 - 缺点: 无法直接访问 UI。默认情况下,事件处理程序在后台线程运行,如果需要操作 UI,需要使用
SynchronizationContext或Control.Invoke。 - 示例应用: 服务中定期写日志文件、检查外部设备状态、易于管理的周期性任务。
3. UI 计时器:System.Windows.Forms.Timer (WinForms,消息循环)
- 优点: 自动保证线程安全,可直接操作 WinForms 控件。
- 缺点: 精度低(最低可达 50ms 左右,在繁忙时更差)。
- 示例应用: 简单的时钟显示、状态栏更新、控制简单的 WinForms 动画。
4. UI 计时器:System.Windows.Threading.DispatcherTimer (WPF,调度器)
- 优点: 自动保证线程安全,可直接操作 WPF 控件;精度相对较高;可以设置
DispatcherPriority来优化性能。 - 缺点: 仅限 WPF/UWP/WinUI 应用。
- 示例应用: 复杂的 WPF 动画、实时图表更新、用户操作的防抖处理。
5.3. 如何进行跨线程操作 (后台 -> UI)
如果你必须使用高精度的后台计时器(System.Threading.Timer 或 System.Timers.Timer)来触发 UI 更新,你必须手动切换到 UI 线程。
- 在 WinForms 中: 使用
Control.Invoke()或Control.BeginInvoke()。 - 在 WPF 中: 使用
Dispatcher.Invoke()或Dispatcher.BeginInvoke()。
跨线程访问 UI 示例(WPF)
csharp
// 假设这是在 WPF 中使用后台计时器 (System.Timers.Timer)
private static System.Timers.Timer _aTimer;
private TextBlock _statusText; // UI 控件
private void InitializeTimer(System.Windows.Threading.Dispatcher dispatcher)
{
_aTimer = new System.Timers.Timer(100); // 高频后台计时器
_aTimer.Elapsed += (s, e) =>
{
// **错误的做法**:_statusText.Text = "更新"; (会抛出跨线程异常)
// **正确的做法**:使用 Dispatcher 切换回 UI 线程
dispatcher.Invoke(() =>
{
_statusText.Text = $"后台计时器触发于: {DateTime.Now.ToLongTimeString()}";
});
};
_aTimer.Start();
}
6.Stopwatch类 (System.Diagnostics.Stopwatch)
Stopwatch 不是一个定时器(Timer),它不会在固定间隔触发事件或回调。它是一个高精度、高性能的工具,专门用于测量代码执行的时间间隔(即耗时)。
Stopwatch 实例可以测量一个时间间隔的运行时间,也可以测量多个时间间隔的总运行时间。 在典型的 Stopwatch 方案中,先调用 Start 方法,然后调用 Stop 方法,最后使用 Elapsed 属性检查运行时间。
Stopwatch 实例或者在运行,或者已停止;使用 IsRunning 可以确定 Stopwatch 的当前状态。使用 Start 可以开始测量运行时间;使用 Stop 可以停止测量运行时间。通过属性 Elapsed、ElapsedMilliseconds 或 ElapsedTicks 查询运行时间值。当实例正在运行或已停止时,可以查询运行时间属性。运行时间属性在 Stopwatch 运行期间稳固递增;在该实例停止时保持不变。
默认情况 下,Stopwatch 实例的运行时间值相当于所有测量的时间间隔的总和。每次调用 Start 时开始累计运行时间计数;每次调用 Stop 时结束当前时间间隔测量,并冻结累计运行时间值。使用 Reset 方法可以清除现有 Stopwatch 实例中的累计运行时间。
Stopwatch在基础计时器机制中对计时器的刻度进行计数,从而测量运行时间。如果安装的硬件和操作系统支持高分辨率性能的计数器,则 Stopwatch 类将使用该计数器来测量运行时间;否则,Stopwatch 类将使用系统计数器来测量运行时间。使用 Frequency 和 IsHighResolution 字段可以确定实现 Stopwatch 计时的精度和分辨率。
核心用途
Stopwatch 最主要的用途是性能分析(Profiling)和代码耗时测量。它可以精确测量一段代码块或一个操作从开始到结束所花费的实际时间。
关键特点
- 高精度:
Stopwatch使用系统的高分辨率性能计数器(如果可用),通常以纳秒(nanosecond)级别计时,因此它的精度远高于前面讨论的四种计时器。 - 不涉及线程/消息循环: 它是一个独立的测量工具,与计时器的线程模型(UI 线程或线程池)无关,它只是记录时间戳。
- 简单易用: 通过
Start()、Stop()和Reset()等方法进行控制。
主要方法和属性
| 方法/属性 | 类型 | 描述 |
|---|---|---|
| Start() | 方法 | 开始或恢复测量时间。 |
| Stop() | 方法 | 停止测量时间。 |
| Reset() | 方法 | 停止测量并将已记录的时间清零。 |
| Restart() | 方法 | 停止、清零,然后重新开始测量。相当于 Reset() + Start()。 |
| IsRunning | 属性 | 获取 Stopwatch 是否正在运行的布尔值。 |
| Elapsed | 属性 | 获取当前已过去的时间间隔,返回一个 TimeSpan 对象。 |
| ElapsedMilliseconds | 属性 | 获取当前已过去的总毫秒数(long)。 |
| ElapsedTicks | 属性 | 获取当前已过去的总计数器刻度数(long)。 |
示例:测量代码块耗时
这个例子演示了如何使用 Stopwatch 来测量一个简单的循环操作的执行时间。
csharp
using System;
using System.Diagnostics;
using System.Threading;
public class StopwatchExample
{
public static void Run()
{
// 1. 创建并启动一个新的 Stopwatch 实例
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Console.WriteLine("Stopwatch 已启动...");
// 2. 执行需要计时的代码块(模拟一个耗时操作)
long sum = 0;
for (int i = 0; i < 100000; i++)
{
sum += i;
// 模拟一些计算延迟
Thread.SpinWait(50);
}
// 3. 停止测量
stopwatch.Stop();
// 4. 读取结果
TimeSpan ts = stopwatch.Elapsed;
Console.WriteLine($"\n代码块执行完毕,计算结果为: {sum}");
Console.WriteLine("------------------------------------------");
Console.WriteLine($"总耗时 (TimeSpan): {ts}");
Console.WriteLine($"总耗时 (毫秒): {stopwatch.ElapsedMilliseconds} ms");
Console.WriteLine($"总耗时 (Ticks): {stopwatch.ElapsedTicks} ticks");
// 5. 重新开始测量 (可选)
stopwatch.Restart();
Console.WriteLine("\nStopwatch 已 Restart。");
// ... 执行另一个操作 ...
// stopwatch.Stop();
}
}
Stopwatch vs. Timer
| 特性 | Stopwatch | Timer (所有类型) |
|---|---|---|
| 用途 | 测量已发生的时间间隔(耗时)。 | 在未来某个时间点或周期性地触发一个动作。 |
| 机制 | 记录高性能计数器的值。 | 依赖操作系统或消息循环的调度。 |
| 精度 | 极高(纳秒级),不受 CPU 负载影响。 | 中等到较低(毫秒级),受线程模型和负载影响。 |
| 事件/回调 | 无。仅提供时间数据。 | 有。通过事件 (Tick/Elapsed) 或回调 (TimerCallback) 触发代码执行。 |
总结来说:如果你想知道一个操作花了多长时间,请用 Stopwatch ;如果你想在某个时间点或周期性地做某事,请用 Timer。
7.精确计时器
7.1. 调用WIN API中的GetTickCount
[DllImport("kernel32")]static extern uint GetTickCount();
从操作系统启动到现在所经过的毫秒数,精度为1毫秒,经简单测试发现其实误差在大约在15ms左右
介绍
- 机制: 调用 Windows 内核 API,返回系统启动后经过的毫秒数。
- 精度: 宣称 1 毫秒。但在实际 Windows 环境中,其底层时钟周期(Timer Resolution)通常默认在 10ms 到 15.6ms 左右,所以您测试出的 15ms 左右的误差是正常的。
- 用途: 主要用于判断系统是否运行了足够长的时间,或者进行非常粗略的时间间隔测量。
优点与缺点
- 优点: 简单易用,不需要特殊权限。
- 缺点:
- 精度低: 不适用于需要高精度测量的场景。
- 溢出限制: 返回值为
uint(32位无符号整数),在大约 49.7 天后会溢出归零。
总结
场景: 粗略计时、判断超时(短于 49 天)。不适用于性能测量或高精度定时。
用法:
csharp
// uint s1 = GetTickCount();Thread.Sleep(2719);Console.WriteLine(GetTickCount() - s1); //单位毫秒
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class GetTickCountExample
{
[DllImport("kernel32")]
static extern uint GetTickCount();
public static void MeasureSleep()
{
uint start = GetTickCount();
Thread.Sleep(1234);
uint end = GetTickCount();
// 由于返回 uint,可以直接相减得到毫秒差
uint elapsedMs = end - start;
Console.WriteLine("--- 1. GetTickCount ---");
Console.WriteLine($"测量耗时: {elapsedMs} 毫秒");
Console.WriteLine($"**注意:** 精度低 (通常 ~10-15ms),结果可能不精确。");
}
}
7.2. timeGetTime (WIN API - 可调精度计时)
csharp
[DllImport("winmm")]static extern uint timeGetTime();
常用于多媒体定时器中,与GetTickCount类似,也是返回操作系统启动到现在所经过的毫秒数,精度为1毫秒。
一般默认的精度不止1毫秒(不同操作系统有所不同),需要调用timeBeginPeriod与timeEndPeriod来设置精度
介绍
- 机制: 与
GetTickCount类似,也是返回系统启动后经过的毫秒数。但它属于多媒体扩展的一部分。 - 精度: 默认精度与
GetTickCount类似。 - 核心特性: 可以通过调用
timeBeginPeriod(t)和timeEndPeriod(t)来修改系统计时器分辨率 。将分辨率设置为 1ms 后,timeGetTime的精度会得到显著提高(以及您观察到的GetTickCount和Environment.TickCount的精度也会提高)。
优点与缺点
- 优点: 通过
timeBeginPeriod可以临时提升系统计时器的精度,对于需要 1ms 精度的应用(如多媒体处理)非常有用。 - 缺点:
- 精度提升代价: 提升精度会增加系统的功耗和 CPU 负载(因为内核时钟中断频率增加)。
- 溢出限制: 同样受限于 32 位返回值(约 49.7 天)。
总结
场景: 1ms 精度定时、多媒体应用。注意: 使用完毕后必须 调用 timeEndPeriod 恢复系统默认精度。
csharp
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class TimeGetTimeExample
{
[DllImport("winmm")]
static extern uint timeGetTime();
[DllImport("winmm")]
static extern void timeBeginPeriod(int t);
[DllImport("winmm")]
static extern void timeEndPeriod(int t);
public static void MeasureSleepWithHighRes()
{
// 尝试将系统计时器分辨率设置为 1 毫秒
timeBeginPeriod(1);
uint start = timeGetTime();
Thread.Sleep(1234);
uint end = timeGetTime();
timeEndPeriod(1); // 务必恢复系统默认精度
uint elapsedMs = end - start;
Console.WriteLine("\n--- 2. timeGetTime (高精度模式) ---");
Console.WriteLine($"测量耗时: {elapsedMs} 毫秒");
Console.WriteLine($"**注意:** 精度已通过 timeBeginPeriod 提升。");
}
}
7.3. System.Environment.TickCount (.NET 封装 - 粗略计时)
介绍
- 机制: .NET 运行时对系统 API 的封装。它在内部确实调用了类似的系统启动时间 API(很可能就是 GetTickCount)。
- 返回值疑问解答:
- GetTickCount 和 timeGetTime 原型返回 DWORD (32位无符号),在 C# 中对应 uint。
System.Environment.TickCount返回int(32位有符号)。- 原因: .NET 框架为了简便和兼容性,将返回值强制转换为
int。这意味着它在约 24.8 天时就会从最大正数溢出到最小负数。对于测量时间差来说,只要时间差小于 24.8 天,减法计算是正确的(因为溢出遵循模运算),但处理起来更麻烦。
优点与缺点
- 优点: 最简单的 .NET 调用方式,不需要 P/Invoke。
- 缺点:
- 精度低: 默认精度与
GetTickCount相同。 - 溢出更早: 溢出周期比
GetTickCount短一半(约 24.8 天)。
- 精度低: 默认精度与
总结
场景: 最简单的粗略计时,不推荐用于生产环境的长时间运行系统或高精度测量。
csharp
using System;
using System.Threading;
public class EnvironmentTickCountExample
{
public static void MeasureSleep()
{
// 返回值是 int
int start = Environment.TickCount;
Thread.Sleep(1234);
int end = Environment.TickCount;
// C# 的 int 减法可以正确处理 32 位溢出,只要时间差小于 24.8 天
int elapsedMs = end - start;
Console.WriteLine("\n--- 3. System.Environment.TickCount ---");
Console.WriteLine($"测量耗时: {elapsedMs} 毫秒");
Console.WriteLine($"**注意:** 精度低,且 24.8 天后溢出。");
}
}
7.4. QueryPerformanceCounter (Windows API - 高分辨率计数器)
介绍
- 机制: 调用 Windows 内核 API,获取硬件级别的高性能计数器的当前值。
- 频率 (
QueryPerformanceFrequency): 获取该计数器每秒增加的次数,通常为 MHz 级别(百万赫兹)。 - 精度: 极高,通常可达纳秒或微秒级别,是 Windows 提供的最高精度计时工具。
优点与缺点
- 优点: 精度高,返回值是
long(64位),几乎不会溢出。 - 缺点:
- 兼容性问题: 需要硬件支持(虽然现代硬件几乎都支持)。
- 硬件依赖性: 您提到的"节能模式/超频模式/电源模式"问题确实存在于较旧的或配置不良的系统上。这些问题通常与 CPU 频率变化和多核系统的计数器同步有关。现代 Windows 版本和硬件(如使用 HPET/ACPI-PM)已大大缓解这些问题,但在跨机器测试时仍需注意。
总结
场景: 毫秒以下级别的高精度性能测量,是实现高精度计时的核心。
使用 P/Invoke 调用 QueryPerformanceFrequency 获取频率,再调用 QueryPerformanceCounter 测量时间。
csharp
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class QPCExample
{
[DllImport("kernel32.dll")]
static extern bool QueryPerformanceFrequency(ref long lpPerformanceFrequency);
[DllImport("kernel32.dll")]
static extern bool QueryPerformanceCounter(ref long lpPerformanceCount);
public static void MeasureSleep()
{
long frequency = 0;
QueryPerformanceFrequency(ref frequency);
long startCount = 0;
QueryPerformanceCounter(ref startCount);
Thread.Sleep(1234);
long endCount = 0;
QueryPerformanceCounter(ref endCount);
// 计算公式:秒 = (计数差 / 频率)
decimal elapsedSeconds = (decimal)(endCount - startCount) / (decimal)frequency;
Console.WriteLine("\n--- 4. QueryPerformanceCounter ---");
Console.WriteLine($"频率: {frequency} 计数/秒");
Console.WriteLine($"测量耗时: {elapsedSeconds:F6} 秒");
Console.WriteLine($"**特点:** 极高精度,以硬件频率为基准。");
}
}
7.5. System.Diagnostics.Stopwatch (.NET 推荐 - 高精度封装)
介绍
- 机制: 这是 .NET 推荐的计时器类。它内部自动封装和管理 了
QueryPerformanceCounter。IsHighResolution属性可以告诉你底层是否使用了QueryPerformanceCounter(通常为true)。- 如果不支持高性能计数器,它会回退到使用
DateTime.Ticks或其他较慢但可靠的系统计数器。
- 优点: 结合了
QueryPerformanceCounter的高精度和 .NET 的易用性,并自动处理了复杂的 API 调用和兼容性问题。
优点与缺点
- 优点:最佳实践。高精度、易用、线程安全、自动处理底层兼容性。
- 缺点: 依赖底层硬件的稳定性,因此继承了
QueryPerformanceCounter潜在的硬件依赖性问题,但在应用层面这是最可靠的选择。
总结
场景: .NET 环境下进行代码性能测量、任何需要高精度计时的场景。
使用 .NET 推荐的 Stopwatch 类。这是对 QueryPerformanceCounter 的最佳封装。
csharp
using System;
using System.Diagnostics;
using System.Threading;
public class StopwatchExample
{
public static void MeasureSleep()
{
Stopwatch sw = new Stopwatch();
Console.WriteLine("\n--- 5. System.Diagnostics.Stopwatch (推荐) ---");
Console.WriteLine($"高分辨率支持: {Stopwatch.IsHighResolution}");
Console.WriteLine($"频率: {Stopwatch.Frequency} 计数/秒");
sw.Start();
Thread.Sleep(1234);
sw.Stop();
// 直接获取 TimeSpan
TimeSpan elapsedTs = sw.Elapsed;
Console.WriteLine($"测量耗时 (TimeSpan): {elapsedTs}");
Console.WriteLine($"测量耗时 (秒): {sw.ElapsedTicks / (double)Stopwatch.Frequency:F6} 秒");
Console.WriteLine($"**特点:** 高精度,易用,.NET 性能测量的首选。");
}
}
7.6. CPU 时间戳 (Read Time-Stamp Counter, RDTSC)
介绍
- 机制: 直接读取 CPU 内部的时间戳计数器 (TSC)。这是一个在每个时钟周期递增的寄存器。
- 精度: 理论上的最高精度,可达纳秒 (ns) 级别。
- 实现: 需要使用内联汇编 (如您所示的 C++ 托管代码)或特殊的外部库来实现
RDTSC指令。
优点与缺点
- 优点: 极高的理论精度和最小的 API 调用开销。
- 缺点:
- 复杂性: 需要 C++/汇编代码,不易于纯 C# 项目使用。
- 不稳定/不可靠: 这是最大的问题。TSC 的频率受 CPU 频率缩放(SpeedStep、Turbo Boost、节能模式等)的影响。在多核/多 CPU 系统中,不同核心的 TSC 可能不同步。这使得它在现代通用操作系统中不适合作为可靠的时间测量基准。
- 需要校准: 必须使用
QueryPerformanceFrequency的值来将时钟周期(Ticks)转换为实际时间(秒),这使得它并没有比QueryPerformanceCounter简单多少。
总结
场景: 仅适用于底层驱动程序、嵌入式系统或非常专业的微基准测试 ,且需要严格控制 CPU 频率和核心同步的环境。不建议在普通应用程序中使用。
由于 C# 不支持内联汇编,此方法需要依赖 C++ 编译的 DLL,在纯 C# 环境中实现过于复杂且不推荐 。这里仅提供一个简化的 C# 调用 示例,假设已经有了一个能够获取 TSC 计数值的 GetCycleCount() 方法。
csharp
// 假设已经通过 P/Invoke 导入了一个外部 DLL 中的 GetCycleCount 方法
// 假设 QueryPerformanceFrequency 已经获取了 QPC 的频率
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class RDTSCExample
{
// 假设这是从 C++ DLL 导入的方法
[DllImport("MyTimerDll.dll")]
static extern ulong GetCycleCount();
public static void MeasureSleep(long qpcFrequency)
{
try
{
ulong startCycles = GetCycleCount();
Thread.Sleep(1234);
ulong endCycles = GetCycleCount();
// 计算公式:秒 = (周期差 / 频率),这里的频率应该是 CPU 的实际运行频率
// 但为简化,我们使用 QPC 的频率进行粗略转换(实际中这种混合转换会导致不准确)
decimal elapsedSeconds = (decimal)(endCycles - startCycles) / (decimal)qpcFrequency;
Console.WriteLine("\n--- 6. CPU 时间戳 (RDTSC) ---");
Console.WriteLine($"测量周期数: {endCycles - startCycles}");
Console.WriteLine($"测量耗时 (基于QPC频率): {elapsedSeconds:F6} 秒");
Console.WriteLine($"**特点:** 理论精度最高,但**结果极不稳定**且难以在纯 C# 中使用。");
}
catch (DllNotFoundException)
{
Console.WriteLine("\n--- 6. CPU 时间戳 (RDTSC) ---");
Console.WriteLine("跳过:该方法需要外部 C++ 编译的 DLL,通常不用于常规 C# 应用。");
}
}
}
最终结论和对比
| 方法 | 类型 | 精度级别 | 溢出/限制 | 推荐场景 |
|---|---|---|---|---|
| GetTickCount | WIN API | 粗糙 (~10-15ms) | 49.7 天溢出 | 最简单的粗略计时 |
| timeGetTime | WIN API | 粗糙,可提升至 1ms | 49.7 天溢出 | 需 1ms 精度且可接受功耗增加的多媒体应用 |
| Environment.TickCount | .NET 封装 | 粗糙 (~10-15ms) | 24.8 天溢出 | 不推荐使用 |
| QueryPerformanceCounter | WIN API | 极高 (微秒/纳秒) | 64 位,无溢出 | 底层高精度计时实现 |
| System.Diagnostics.Stopwatch | .NET 封装 | 极高 (微秒/纳秒) | 64 位,无溢出 | 常规应用中高精度测量的首选 |
| CPU Time Stamp (RDTSC) | 汇编指令 | 理论最高 (纳秒) | 不稳定,需 C++ | 仅用于专业微基准测试或内核级应用 |
对于常规应用中的定时触发,System.Timers.Timer (结合 timeBeginPeriod)或 System.Windows.Threading.DispatcherTimer 是首选。
对于测量代码执行时间,System.Diagnostics.Stopwatch 是当之无愧的最佳和最简单的选择。