Web常用工具 二维码美化 在线压缩 JavaScript JSON工具 JavaScript AI工具汇总网站 BASE64转换 狂神博客园 图片轮播插件(swiper) W3CHTML W3SCHOOL TypeScript 开源中国 51aspx github codeproject SQLBACKUP 几种排序算法比较 SQL中deny权限 MSDN下载 HttpWebRequest类 HTML5 stackoverflow ASP.NET 页生命周期概述 IIS 5.0 和 6.0 的 ASP.NET 应用程序生命周期概述 [SQL Server]视图sys.sysprocesses brnshop学习 SQL视频 Fiddler帮助文档 Sprint.Net SQLServer牛人博客 codeplex IIS官网 IE11 Developer Jquery源码视频上 Jquery源码视频下 Jquery Bugs jquery.miaov.com 正则表达式 Jquery API 1.9 Service Broker Javascript Object中的方法讲解 Microsoft webcast 微信开发接口 ECMAScript5 Underscore Jquery Bugs SQL存储过程事务嵌套 官网SQL事务锁 2345天气插件 Json数据查看 Javascript设计模式 C++ jquery-plugin-validate 博学谷(传智播客) Swift视频 IOS代码论坛 SWIFT设计模式 操作系统下载 AngularJS VueJS Nuxt Vant-UI官方文档 ASPNETCORE 前端题库 Node.js NPMjs ASPNETCORE 腾讯课堂 SwiftUI SwiftUI疑问解答 ADO.NET SMO 数字化企业网 Unicode码查询 Redis使用文档 .NET 微服务:适用于容器化 .NET 应用程序的体系结构 .NETCore5.0微软官方文档 CSS3.0 在 ASP.NET Core 中配置 Windows 身份验证 Maven使用教程 Maven Repository Thymeleaf Thymeleaf中文CSDN Spring官方文档 SpringBoot SpringData SVG在线设计工具 SVG教程01 SVG教程02 fontawesome图标库 mybatis官网 mybatis-spring中文网 mysql教程 python教程 python的scrapy教程01 python的scrapy教程02 VS开发python xpath教程 腾讯向量数据库教程 JSZip浏览器内存中创建文件与文件夹 axios的使用文档 SheetJS(JS操作excel)的使用文档 极简插件官网(chrome的扩展插件) 金蝶云星空学习成长 常用接口调用 ThreeJS中文网 D3.js官网 anime.js官网 gsap.js官网 xlsx.js官网 若依框架 若依文档 华为数字人 MDN之JavaScript语法 百度地图API 百度地图API案例 百度地图API使用说明 Nginx中文文档 i18n Animate.css Bootstrap官网 Jquery datatables.net插件 免费SVG C#官网 phet小学初中高中数理化 Pixi.js中文网 webgl教程 unity资源商店国际版 unity官方文档 unity官方着色器Shader unity官方论坛 微信官方文档

huaan011

 

C#中用Task.Run+await Task.Delay 与 Timer 的性能比较

Task.Run()+Task.Delay()实现指定的时间执行某个方法 AddOrderForAutoCancel() 大致代码如下:

 Task.Run(async () => { 
  while (true) { 
    var dueTime = absoluteExpiration - DateTimeOffset.Now + TimeSpan.FromSeconds(1);

    if (dueTime < TimeSpan.Zero)
    {
        dueTime = TimeSpan.FromSeconds(1); //TimeSpan.Zero,可能发生死循环
    }

    if (_cache.TryGetValue(cacheKey, out var val))
    { 
      GDCLogHelper.AddLog($"缓存仍有效,Key={cacheKey}", "RegisterPostEvictionCallback", Modules.OrderCache); 
    } 
    else { 
      GDCLogHelper.AddLog($"缓存已过期,Key={cacheKey}", "RegisterPostEvictionCallback", Modules.OrderCache); 
      //此处开始执行指定时间要执行的方法 
      Callback();
      break; 
    } 
    await Task.Delay((int)Math.Round(dueTime.TotalMilliseconds)); // 指定间隔时间秒访问  
  } 
}
);

用 Timer执行指定时间要指定的方法:

Timer _timer; 
// 设置定时器,提前1秒触发,保证尽量准时执行回调
                var dueTime = _absoluteExpiration - DateTimeOffset.Now - TimeSpan.FromSeconds(1);
                if (dueTime < TimeSpan.Zero)
                    dueTime = TimeSpan.Zero;

                 _timer?.Dispose();
                _timer = new Timer(TimerCallback, null, dueTime, Timeout.InfiniteTimeSpan); //TimerCallback就是指定时间要执行的方法

虽然都可以实现指定时间执行指定的方法,但是有很大的性能区别。

具体致命问题:

1. 线程池爆炸与性能崩塌

Task.Run(async () => { while (true) { ... await Task.Delay(...); } });
  • 问题‌每调用一次 AddOrderForAutoCancel,就启动一个新的长期运行的后台任务。
  • ‌后果‌:如果有 10,000 个未付款订单,就有 10,000 个后台任务在运行。虽然 Task.Delay 不阻塞线程,但它会频繁地让线程池线程唤醒、检查状态、再休眠。这会极大地增加线程池的调度压力,导致其他重要业务请求(如用户登录、下单)响应变慢甚至超时。
  • ‌对比‌:System.Threading.Timer 是轻量级的,10,000 个 Timer 可能只需要几个线程池线程就能高效管理。Timer 本身的触发误差通常在 ‌15-50ms‌。System.Threading.Timer 基于 Windows 多媒体定时器或线程池计时器。在标准 Windows 环境下,定时器的最小分辨率通常为 ‌15.6ms‌(默认时钟中断间隔)。通过调用 timeBeginPeriod(1) 可以将系统时钟精度提高到 ‌1ms‌,但 .NET Timer 并不总是能保证 1ms 的绝对准时,受限于线程池调度。
  • 在 .NET 底层,Timer 是基于线程池计时器队列实现的。即使有数万个定时任务,它们也共享少量的线程池线程进行调度,而不是为每个任务创建独立线程或长期占用线程。
  • 相比之下,使用 Task.Delay 轮询的方案会在高并发下产生大量的状态机对象和频繁的线程上下文切换,导致线程池耗尽。

2. “死循环”与资源泄漏风险

  • ‌问题‌:while(true) 没有外部取消机制(如 CancellationToken)。如果由于 Bug 导致 _cache.TryGetValue 始终返回 true(例如缓存未真正过期或 Key 匹配错误),这个任务将‌永远运行下去‌,永不退出。
  • ‌后果‌:内存中堆积大量无效的 Task 状态机对象,最终导致 OutOfMemoryException。
  • ‌对比‌:方案一/二在回调执行后或手动移除时,会显式 Dispose Timer 并从字典中移除状态,确保资源释放。

3. 时间精度与误差不可控

  • ‌问题‌:dueTime 是静态计算的,且 Task.Delay 本身有最小分辨率限制(通常约 15ms,取决于系统定时器精度)。随着循环次数增加,累积误差可能变大。
  • ‌对比‌:依赖 MemoryCache 的原生过期机制,由框架底层保证过期时间的准确性,Timer 仅做极短时间的兜底,误差控制在毫秒级。

 

 

其过期回调的执行误差通常可以控制在 ‌10毫秒 ~ 50毫秒‌ 之间,极端情况下可能达到 ‌100毫秒左右‌,但绝不会像 Thread.Sleep 或普通轮询那样出现秒级误差。

以下是详细的精度分析与影响因素:

1. 理论精度分析

该方案采用了 ‌“MemoryCache 原生过期 + Timer 兜底”‌ 的双重机制,精度主要取决于以下两个部分:

A. 主路径:MemoryCache 原生过期(精度最高)

  • ‌机制‌:MemoryCache 内部使用高分辨率计时器检查过期时间。当缓存项到达 AbsoluteExpiration 时,它会在下一次缓存访问或内部清理扫描时被标记为过期,并立即触发 PostEvictionCallback
  • ‌误差来源‌:
    • ‌被动触发延迟‌:如果没有其他线程访问缓存,MemoryCache 依赖后台清理线程(Cleaner Thread)。这个后台线程默认每隔一定时间(通常是几秒或更短,取决于负载)运行一次。这可能导致回调延迟几秒。
    • ‌主动触发即时性‌:在你的代码中,有一个 ‌Timer 兜底机制‌。Timer 会在过期前 1 秒唤醒,调用 _cache.TryGetValue(key, out _)。‌这一行代码是关键‌,它强制 MemoryCache 立即检查该 Key 的状态。如果此时已过期,PostEvictionCallback 会被‌同步、立即‌触发。
  • ‌结论‌:由于有 Timer 强制触发检查,主路径的延迟主要取决于 ‌Timer 的唤醒精度‌ 和 ‌CPU 调度延迟‌,通常在 ‌10-30ms‌ 左右。

B. 兜底路径:System.Threading.Timer(精度较高)

  • ‌机制‌:Timer 设置为在过期前 1 秒触发。如果缓存因某种原因未被原生机制移除(极罕见),Timer 回调会检查缓存是否存在,若不存在则执行回调。
  • ‌误差来源‌:
    • ‌Windows Timer 分辨率‌:System.Threading.Timer 基于 Windows 多媒体定时器或线程池计时器。在标准 Windows 环境下,定时器的最小分辨率通常为 ‌15.6ms‌(默认时钟中断间隔)。通过调用 timeBeginPeriod(1) 可以将系统时钟精度提高到 ‌1ms‌,但 .NET Timer 并不总是能保证 1ms 的绝对准时,受限于线程池调度。
    • ‌线程池排队‌:Timer 回调是在线程池线程上执行的。如果线程池繁忙,回调可能需要排队等待,这会引入额外的几毫秒到几十毫秒延迟。
  • ‌结论‌:Timer 本身的触发误差通常在 ‌15-50ms‌。

2. 综合误差评估

表格
场景预期误差范围说明
‌正常负载‌ ‌10ms - 30ms‌ Timer 准时唤醒 -> 强制检查缓存 -> 立即触发回调。这是最常见的情况。
‌高负载/CPU繁忙‌ ‌30ms - 100ms‌ 线程池排队导致 Timer 回调延迟,或 GC 暂停导致调度延迟。
‌极端情况‌ ‌> 100ms‌ 发生 Full GC(Stop-The-World),或操作系统进入省电模式降低时钟频率。
‌对比 Task.Delay 轮询‌ ‌秒级误差‌ 原方案三若轮询间隔设为 1s,误差可达 1s;若设为 10ms,CPU 爆满。本方案远优于此。

3. 为什么不能做到“微秒级”或“绝对 0 误差”?

根据搜索结果中的原理,C# 运行在通用操作系统(Windows/Linux)之上,而非实时操作系统(RTOS):

  1. ‌非实时调度‌:操作系统线程调度器无法保证你的回调线程在精确的某一微秒被唤醒。它只能保证“大约”在那个时间点准备好运行。
  2. ‌GC 干扰‌:.NET 的垃圾回收(GC)可能会暂停所有托管线程(Stop-The-World),这会导致几毫秒到几百毫秒的不确定性延迟。
  3. ‌Timer 底层限制‌:即使使用高精度 API,Windows 默认的时钟中断间隔是 15.6ms。虽然可以调整,但 .NET 的 Timer 封装并未暴露这种底层控制,且频繁的高精度计时会增加 CPU 功耗和上下文切换开销。

4. 如何进一步优化精度?(如果需要 < 10ms)

如果你的业务对精度要求极高(例如高频交易、工业控制),可以尝试以下优化,但需权衡资源消耗:

  1. ‌提高系统时钟精度‌:
    在应用启动时调用 Windows API timeBeginPeriod(1),将系统全局时钟精度提高到 1ms。这会显著改善 Timer 的准时性,但会增加整个系统的功耗和上下文切换频率。

    csharp
     
    [DllImport("winmm.dll")] static extern uint timeBeginPeriod(uint uPeriod); // 在 Program.cs 或 Startup 中调用 timeBeginPeriod(1);
  2. ‌缩短 Timer 兜底间隔‌:
    将代码中的 TimeSpan.FromSeconds(1) 改为 TimeSpan.FromMilliseconds(100) 甚至更短。

    • 优点:Timer 更频繁地检查,能更早发现过期。
    • 缺点:增加了 Timer 回调的频率,轻微增加 CPU 负担。但对于大量订单场景,建议保持在 500ms-1s,因为 MemoryCache 的原生机制已经足够快,Timer 只是保险。
  3. ‌使用 SpinWait 进行最后阶段的忙等待(不推荐用于Web服务)‌:
    在 Timer 回调中,如果发现距离过期时间还有几毫秒,可以使用 SpinWait.SpinUntil 进行忙等待。这会占用 CPU 核心,但能提供微秒级的最后冲刺精度。

    • 警告:这在 Web 服务器(如 ASP.NET Core)中是‌极度危险‌的,会阻塞线程池,导致其他请求超时。仅适用于专用的后台控制台程序。

总结

对于‌订单自动取消、验证码过期‌这类业务场景,‌10-50ms 的误差是完全可接受的‌,甚至可以说是“极其精确”的。用户感知不到几十毫秒的差异,而数据库事务的一致性才是关键。

posted on 2026-09-20 17:09  华安  阅读(2)  评论(0)    收藏  举报

导航