惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
B
Blog
博客园 - Franky
H
Help Net Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
云风的 BLOG
云风的 BLOG
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
V
V2EX
Martin Fowler
Martin Fowler
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队

博客园 - 华安

C#中Microsoft.Extensions.Caching.Memory 与 System.Runtime.Caching.MemoryCache区别 自适应网格系统:CSS Grid中repeat()、auto-fill与auto-fit的深度解析 CSS3中响应式布局两大神器display:flex和display:grid springBoot中的 pom.xml文件 bulid学习 CSS中元素的display显示方式有多种,隐藏、块级、内联、内联-块级 SQl Server 中的 go 是什么作用 CSS中 display:flex的align-items: stretch; 移动端浏览器(尤其是 iOS Safari)的橡皮筋回弹效果(Overscroll / Bounce Effect) 手机端浏览器上ES6中的Fetch回调执行 window.open没效果 用Flex实现兼容性好的全屏布局 在 VS Code 中使用 C# Dev Kit 和 Unity Tools 调试 Unity 2022 Unity 可编程物件(ScriptableObject) 微信小程序中的 联系客服 最基本的使用方法 wx.requestSubscribeMessage(Object object) 和 wx.requestSubscribeDeviceMessage(Object object) 这两个有什么区别 微信小程序中 wx.hideLoading() 后调用 wx.showToast()的问题 Windows 中启动 Nginx的常用命令 CSS进阶技巧:字体渐变、描边、倒影与渐变色描边全解析 netCore 中各DLL引用了 SkiaSharp.dll的问题 unity中预制体解包 在 Unity 中,Time.timeScale实现游戏暂停加速等 微信小程序中关联微信支付 unity中的 Navigation AI使用 C#中TaskCompletionSource(简称 TCS)学习 Unity 编辑器 中,快捷键 Ctrl + Shift + F 的功能 unity中按下 F键,让物体聚焦 微信中进入定页面的,判断时通过扫二维码进入的,还是点小程序名称进入的 MYSQL中从JSON字符串中提取指定的值 Unity2022中创建动画 Animation(旧方法) Unity 中区别,public 和 [SerializeField] Unity中 onCollisionEnter2D与OnTriggerEnter2D 区别
C#中用Task.Run+await Task.Delay 与 Timer 的性能比较
华安 · 2026-09-20 · via 博客园 - 华安

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 的准时性,但会增加整个系统的功耗和上下文切换频率。

    [DllImport("winmm.dll")] static extern uint timeBeginPeriod(uint uPeriod);

  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 的误差是完全可接受的‌,甚至可以说是“极其精确”的。用户感知不到几十毫秒的差异,而数据库事务的一致性才是关键。