










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就是指定时间要执行的方法
Task.Run(async () => { while (true) { ... await Task.Delay(...); } });
AddOrderForAutoCancel,就启动一个新的长期运行的后台任务。Task.Delay 不阻塞线程,但它会频繁地让线程池线程唤醒、检查状态、再休眠。这会极大地增加线程池的调度压力,导致其他重要业务请求(如用户登录、下单)响应变慢甚至超时。System.Threading.Timer 是轻量级的,10,000 个 Timer 可能只需要几个线程池线程就能高效管理。Timer 本身的触发误差通常在 15-50ms。System.Threading.Timer 基于 Windows 多媒体定时器或线程池计时器。在标准 Windows 环境下,定时器的最小分辨率通常为 15.6ms(默认时钟中断间隔)。通过调用 timeBeginPeriod(1) 可以将系统时钟精度提高到 1ms,但 .NET Timer 并不总是能保证 1ms 的绝对准时,受限于线程池调度。Task.Delay 轮询的方案会在高并发下产生大量的状态机对象和频繁的线程上下文切换,导致线程池耗尽。while(true) 没有外部取消机制(如 CancellationToken)。如果由于 Bug 导致 _cache.TryGetValue 始终返回 true(例如缓存未真正过期或 Key 匹配错误),这个任务将永远运行下去,永不退出。Task 状态机对象,最终导致 OutOfMemoryException。Dispose Timer 并从字典中移除状态,确保资源释放。dueTime 是静态计算的,且 Task.Delay 本身有最小分辨率限制(通常约 15ms,取决于系统定时器精度)。随着循环次数增加,累积误差可能变大。MemoryCache 的原生过期机制,由框架底层保证过期时间的准确性,Timer 仅做极短时间的兜底,误差控制在毫秒级。其过期回调的执行误差通常可以控制在 10毫秒 ~ 50毫秒 之间,极端情况下可能达到 100毫秒左右,但绝不会像 Thread.Sleep 或普通轮询那样出现秒级误差。
以下是详细的精度分析与影响因素:
该方案采用了 “MemoryCache 原生过期 + Timer 兜底” 的双重机制,精度主要取决于以下两个部分:
MemoryCache 内部使用高分辨率计时器检查过期时间。当缓存项到达 AbsoluteExpiration 时,它会在下一次缓存访问或内部清理扫描时被标记为过期,并立即触发 PostEvictionCallback。MemoryCache 依赖后台清理线程(Cleaner Thread)。这个后台线程默认每隔一定时间(通常是几秒或更短,取决于负载)运行一次。这可能导致回调延迟几秒。_cache.TryGetValue(key, out _)。这一行代码是关键,它强制 MemoryCache 立即检查该 Key 的状态。如果此时已过期,PostEvictionCallback 会被同步、立即触发。System.Threading.Timer 基于 Windows 多媒体定时器或线程池计时器。在标准 Windows 环境下,定时器的最小分辨率通常为 15.6ms(默认时钟中断间隔)。通过调用 timeBeginPeriod(1) 可以将系统时钟精度提高到 1ms,但 .NET Timer 并不总是能保证 1ms 的绝对准时,受限于线程池调度。| 场景 | 预期误差范围 | 说明 |
|---|---|---|
| 正常负载 | 10ms - 30ms | Timer 准时唤醒 -> 强制检查缓存 -> 立即触发回调。这是最常见的情况。 |
| 高负载/CPU繁忙 | 30ms - 100ms | 线程池排队导致 Timer 回调延迟,或 GC 暂停导致调度延迟。 |
| 极端情况 | > 100ms | 发生 Full GC(Stop-The-World),或操作系统进入省电模式降低时钟频率。 |
| 对比 Task.Delay 轮询 | 秒级误差 | 原方案三若轮询间隔设为 1s,误差可达 1s;若设为 10ms,CPU 爆满。本方案远优于此。 |
根据搜索结果中的原理,C# 运行在通用操作系统(Windows/Linux)之上,而非实时操作系统(RTOS):
Timer 封装并未暴露这种底层控制,且频繁的高精度计时会增加 CPU 功耗和上下文切换开销。如果你的业务对精度要求极高(例如高频交易、工业控制),可以尝试以下优化,但需权衡资源消耗:
提高系统时钟精度:
在应用启动时调用 Windows API timeBeginPeriod(1),将系统全局时钟精度提高到 1ms。这会显著改善 Timer 的准时性,但会增加整个系统的功耗和上下文切换频率。
[DllImport("winmm.dll")]
static extern uint timeBeginPeriod(uint uPeriod);
缩短 Timer 兜底间隔:
将代码中的 TimeSpan.FromSeconds(1) 改为 TimeSpan.FromMilliseconds(100) 甚至更短。
MemoryCache 的原生机制已经足够快,Timer 只是保险。使用 SpinWait 进行最后阶段的忙等待(不推荐用于Web服务):
在 Timer 回调中,如果发现距离过期时间还有几毫秒,可以使用 SpinWait.SpinUntil 进行忙等待。这会占用 CPU 核心,但能提供微秒级的最后冲刺精度。
对于订单自动取消、验证码过期这类业务场景,10-50ms 的误差是完全可接受的,甚至可以说是“极其精确”的。用户感知不到几十毫秒的差异,而数据库事务的一致性才是关键。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。