C#定时器选型指南:四种Timer底层机制与避坑实战

发布时间:2026/10/14 21:02:14
C#定时器选型指南:四种Timer底层机制与避坑实战 1. 为什么“定时器”这件小事值得单独拎出来讲做C#开发的人几乎都绕不开定时器。不管你是写WinForm上位机、WPF工具、ASP.NET后台服务还是Unity里做帧同步逻辑总有一个场景需要“每隔一段时间干一件事”。但很多人第一次用定时器的时候都是随手一搜看到System.Timers.Timer就拿来用了跑起来好像也没问题于是就再也没换过。问题往往在项目上线之后才暴露出来UI莫名其妙卡死、定时任务偶尔不触发、程序退出时线程还在后台跑、多个定时器同时操作同一个集合导致数据错乱……这些坑我几乎每一个都踩过。回头复盘才发现根子不在业务逻辑而在于一开始就没搞清楚C#里这几种定时器到底有什么区别各自适合什么场景。C#里常见的定时器大致有这么几类System.Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer以及.NET 6之后引入的PeriodicTimer。它们名字里都带Timer但底层实现、回调线程、精度、生命周期管理完全不同。选错了轻则性能浪费重则出现难以复现的并发Bug。这篇文章适合所有写过或准备写定时逻辑的C#开发者尤其是做桌面端、服务端后台任务、工控上位机这类对时序敏感场景的朋友。我会把这几种定时器的底层机制、回调线程模型、精度表现、资源释放方式逐一拆开讲再配上可直接复现的代码和参数计算过程最后把我自己踩过的坑整理成一份排查清单。看完之后你至少能做到拿到一个定时需求三秒钟判断该用哪个。2. 四种定时器的整体设计与选型思路2.1 先搞清楚一个核心问题回调跑在哪个线程选定时器本质上不是选“哪个能定时”因为它们都能定时。真正的分水岭是回调函数在哪个线程上执行。这一个问题决定了你要不要处理跨线程访问、要不要加锁、UI会不会卡。我用一个生活化的类比来解释。假设你是一个餐厅老板需要每隔10分钟去后厨检查一次汤有没有熬好。这几种定时器的区别就像是你用不同的方式安排这件事System.Windows.Forms.Timer相当于你亲自盯着墙上的钟时间到了你自己起身去后厨。你UI线程被占用了期间没法接待客人。System.Timers.Timer相当于你雇了一个服务员时间到了他去后厨检查然后回来告诉你。你UI线程不用动但服务员回来汇报的时候你得停下手里的活听他讲。System.Threading.Timer相当于后厨自己装了个闹钟时间到了厨师自己检查跟你完全没关系。你该干嘛干嘛但厨师可能会同时动你正在用的食材。PeriodicTimer相当于一个现代化的排班系统厨师按节奏自己循环检查而且支持“我这次还没检查完下一次先别催”。这个类比基本能覆盖80%的选型判断。下面我把每种定时器的关键参数列出来对比。定时器类型所在命名空间回调线程是否支持异步适用场景System.Windows.Forms.TimerSystem.Windows.FormsUI线程否WinForms界面刷新、控件更新System.Timers.TimerSystem.Timers线程池线程否但可手动async服务端后台任务、简单周期任务System.Threading.TimerSystem.Threading线程池线程否但可手动async高性能场景、底层库、无UI依赖PeriodicTimerSystem.Threading调用方指定是现代异步周期任务、.NET 62.2 为什么会有这么多种而不是只留一个这个问题我在早期也想过。后来做的时间久了才明白每一种定时器的存在都有它的历史原因和场景绑定。System.Windows.Forms.Timer是最早的它诞生于WinForms时代核心诉求就是“别让我处理跨线程”。因为WinForms的控件有线程亲和性只有创建控件的线程才能操作它。所以这个定时器干脆把回调直接扔到UI线程的消息循环里你写代码的时候完全不用管Invoke。代价就是精度差、UI忙的时候会延迟。System.Timers.Timer是为了解决服务端场景。服务端没有UI线程的概念它需要的是“到点了在线程池里跑一段逻辑”。它比WinForms Timer精度高而且提供了AutoReset、Enabled这些方便的属性。但它有个坑如果回调执行时间超过了间隔时间它会用新的线程池线程再进来一次导致重入。System.Threading.Timer是最底层的System.Timers.Timer内部其实就是包装了它。它的API比较原始构造函数里就要传回调、状态对象、延迟时间和间隔时间改间隔还得调Change方法。但正因为原始它的开销最小适合在高频场景里用。PeriodicTimer是.NET 6引入的新东西专门为async/await设计。它的WaitForNextTickAsync方法返回一个ValueTask你可以await它而且它天然支持“上一次没处理完就不触发下一次”的语义避免了重入问题。写异步周期任务它是最舒服的。选型的第一原则先看回调要不要碰UI再看回调是不是异步逻辑最后看精度和重入要求。按这个顺序筛基本不会选错。3. 逐个拆解每种定时器的核心细节与实操要点3.1 System.Windows.Forms.TimerUI线程的亲儿子这个定时器只能在WinForms项目里用WPF里对应的是System.Windows.Threading.DispatcherTimer思路类似。它的核心特点是Tick事件在UI线程上触发。这意味着你在Tick事件里可以直接写label1.Text DateTime.Now.ToString()不需要任何Invoke。这是它最大的优势也是唯一的优势。它的精度有多差呢我实测过在UI线程空闲的情况下设置100ms间隔实际触发间隔大概在100~115ms之间波动。如果UI线程正在处理一个耗时操作比如加载一个大文件那Tick会被直接阻塞直到UI线程空闲下来才补触发。注意是“补触发”而不是“跳过”如果你设置了AutoReset它可能会连续触发好几次来追赶。// WinForms Timer 典型用法 private System.Windows.Forms.Timer _uiTimer; private void InitUiTimer() { _uiTimer new System.Windows.Forms.Timer(); _uiTimer.Interval 100; // 单位毫秒 _uiTimer.Tick UiTimer_Tick; _uiTimer.Start(); } private void UiTimer_Tick(object sender, EventArgs e) { // 这里直接操作UI控件安全 lblStatus.Text $更新时间: {DateTime.Now:HH:mm:ss.fff}; }实操要点Interval属性最小值是1但实际精度受限于系统时钟粒度Windows下大约是15.6ms。所以设1ms和设15ms效果差不多。不要在Tick里做耗时操作否则UI直接卡死。如果非要做用async void配合await Task.Run但要注意回到UI线程更新控件。停止定时器用Stop()但已经排队的Tick可能还会执行一次所以Tick里最好加一个if (!_uiTimer.Enabled) return;的保护。我踩过的坑有一次在Tick里同步调用了一个HTTP请求结果UI卡了3秒用户以为程序死了。后来改成async void加await问题解决。但async void的异常捕获很麻烦建议用async Task包装一层。3.2 System.Timers.Timer服务端最常用的那个这个定时器在服务端代码里出现频率最高。它的Elapsed事件默认在线程池线程上触发所以不涉及UI线程安全问题。它提供了AutoReset属性默认是true表示每次触发后自动重新计时设为false就只触发一次。它的一个关键参数是Interval单位也是毫秒。和WinForms Timer不同它的精度要高不少实测100ms间隔下波动在100~105ms左右。但它的精度依然受系统时钟影响做不到硬实时。// System.Timers.Timer 典型用法 private System.Timers.Timer _serverTimer; private void InitServerTimer() { _serverTimer new System.Timers.Timer(); _serverTimer.Interval 1000; _serverTimer.AutoReset true; _serverTimer.Elapsed ServerTimer_Elapsed; _serverTimer.Start(); } private void ServerTimer_Elapsed(object sender, ElapsedEventArgs e) { // 这里在线程池线程上执行 // 如果回调耗时超过Interval会重入 DoSomething(); }重入问题怎么理解假设Interval是1000ms你的回调执行了1500ms。那么在1000ms时第一次触发线程池分配线程A执行到2000ms时线程A还在跑但定时器又到点了线程池会再分配线程B进来执行。这时候就有两个线程同时在跑你的回调逻辑。如果回调里操作了共享集合直接抛InvalidOperationException。解决办法有两个一是把AutoReset设为false在回调结束时手动再Start()一次二是加一个Interlocked标志位做互斥。private int _isRunning 0; private void ServerTimer_Elapsed(object sender, ElapsedEventArgs e) { if (Interlocked.CompareExchange(ref _isRunning, 1, 0) ! 0) return; // 上一次还没跑完直接跳过 try { DoSomething(); } finally { Interlocked.Exchange(ref _isRunning, 0); } }实操要点System.Timers.Timer实现了IDisposable用完必须Dispose()否则定时器会一直持有引用导致内存泄漏。Elapsed事件里如果抛异常定时器不会停但异常会在线程池线程上抛出可能导致进程崩溃。一定要用try-catch包住。修改Interval属性会重置计时如果频繁修改可能导致定时器永远不触发。3.3 System.Threading.Timer最底层也最灵活这个定时器是前两者的底层实现。它的构造函数直接接收回调委托、状态对象、延迟时间和间隔时间。没有事件没有Start/Stop改间隔用Change方法。// System.Threading.Timer 典型用法 private System.Threading.Timer _threadingTimer; private void InitThreadingTimer() { // 参数回调状态对象首次延迟间隔 _threadingTimer new System.Threading.Timer( ThreadingTimer_Callback, null, 0, // 立即开始 500 // 每500ms一次 ); } private void ThreadingTimer_Callback(object state) { // 在线程池线程上执行 DoWork(); } // 停止定时器 private void StopThreadingTimer() { // 传入Timeout.Infinite表示不再触发 _threadingTimer.Change(Timeout.Infinite, Timeout.Infinite); }它的精度和System.Timers.Timer基本一致因为后者就是包装了它。但它的开销更小因为没有事件委托的额外层级。在高频场景下比如每10ms执行一次的任务用System.Threading.Timer会比System.Timers.Timer更省资源。它的一个独特优势是状态对象。你可以在构造时传入一个对象回调里通过state参数拿到。这在需要给回调传参的场景下很方便避免了闭包捕获带来的额外分配。// 用状态对象传参 _threadingTimer new System.Threading.Timer( state { var config (TimerConfig)state; Process(config); }, new TimerConfig { Id 1, Name 任务A }, 0, 1000 );实操要点Change方法可以在任何时候调用用来修改延迟和间隔。传入Timeout.Infinite可以暂停定时器。回调执行时间超过间隔时同样会重入。需要用Interlocked或SemaphoreSlim做互斥。Dispose()不会等待正在执行的回调结束。如果回调里还在用非托管资源需要在回调里加一个CancellationToken检查。这个定时器没有Enabled属性停止只能靠Change(Timeout.Infinite, Timeout.Infinite)或Dispose()。3.4 PeriodicTimer.NET 6之后的现代选择PeriodicTimer是.NET 6引入的专门为异步场景设计。它的用法和前面三种完全不同不是事件驱动而是通过WaitForNextTickAsync方法让你await。// PeriodicTimer 典型用法 private async Task RunPeriodicTaskAsync(CancellationToken ct) { using var timer new PeriodicTimer(TimeSpan.FromSeconds(1)); while (await timer.WaitForNextTickAsync(ct)) { // 这里默认在调用方的上下文中执行 // 如果是在UI线程调用这里就是UI线程 await DoWorkAsync(); } }它的核心优势有三个第一天然支持异步。WaitForNextTickAsync返回ValueTaskbool你可以直接await不会阻塞线程。第二不会重入。如果你上一次的DoWorkAsync还没执行完WaitForNextTickAsync不会返回下一次的tick。它会等你处理完然后重新开始计时。这个语义在大多数业务场景下才是正确的。第三支持取消。传入CancellationToken后调用Cancel()会让WaitForNextTickAsync返回false循环自然退出。但要注意一个细节PeriodicTimer的计时是从你调用WaitForNextTickAsync开始算的而不是从你处理完上一次任务开始算。也就是说如果你的任务执行了800ms间隔是1000ms那么下一次tick会在200ms后到来。如果你希望“任务执行完后再等1000ms”需要自己手动控制。// 如果需要“执行完再等固定时间” while (!ct.IsCancellationRequested) { await DoWorkAsync(); await Task.Delay(1000, ct); }实操要点PeriodicTimer实现了IDisposable但用using声明即可不需要手动调Dispose。它不能像其他定时器那样“修改间隔”间隔在构造时就固定了。要改间隔只能重新创建一个。它不适合高频场景比如低于10ms因为ValueTask和异步状态机的开销比直接线程池回调要大。在UI线程上使用时WaitForNextTickAsync会捕获同步上下文回调默认回到UI线程。如果不想回到UI线程用ConfigureAwait(false)。4. 实操过程与核心环节实现4.1 一个可复现的对比实验四种定时器的精度实测光讲理论不够直观我写了一个简单的控制台程序把四种定时器放在一起跑间隔都设为100ms跑10秒记录每次触发的实际间隔最后统计平均值和标准差。下面是核心代码。using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; class TimerPrecisionTest { static void Main() { Console.WriteLine(开始测试每种定时器运行10秒间隔100ms); Console.WriteLine(----------------------------------------); TestWinFormsTimer(); // 需要WinForms项目这里用DispatcherTimer替代演示 TestTimersTimer(); TestThreadingTimer(); TestPeriodicTimer().Wait(); } static void TestTimersTimer() { var intervals new Listdouble(); var sw new Stopwatch(); var last 0L; var timer new System.Timers.Timer(100); timer.AutoReset true; timer.Elapsed (s, e) { if (last 0) intervals.Add(sw.ElapsedMilliseconds - last); last sw.ElapsedMilliseconds; }; sw.Start(); timer.Start(); Thread.Sleep(10000); timer.Stop(); timer.Dispose(); PrintResult(System.Timers.Timer, intervals); } static void TestThreadingTimer() { var intervals new Listdouble(); var sw new Stopwatch(); var last 0L; var timer new System.Threading.Timer(_ { if (last 0) intervals.Add(sw.ElapsedMilliseconds - last); last sw.ElapsedMilliseconds; }, null, 0, 100); sw.Start(); Thread.Sleep(10000); timer.Change(Timeout.Infinite, Timeout.Infinite); timer.Dispose(); PrintResult(System.Threading.Timer, intervals); } static async Task TestPeriodicTimer() { var intervals new Listdouble(); var sw new Stopwatch(); var last 0L; using var timer new PeriodicTimer(TimeSpan.FromMilliseconds(100)); sw.Start(); var cts new CancellationTokenSource(10000); try { while (await timer.WaitForNextTickAsync(cts.Token)) { if (last 0) intervals.Add(sw.ElapsedMilliseconds - last); last sw.ElapsedMilliseconds; } } catch (OperationCanceledException) { } PrintResult(PeriodicTimer, intervals); } static void PrintResult(string name, Listdouble intervals) { if (intervals.Count 0) return; var avg intervals.Average(); var std Math.Sqrt(intervals.Average(v Math.Pow(v - avg, 2))); Console.WriteLine(${name}:); Console.WriteLine($ 触发次数: {intervals.Count}); Console.WriteLine($ 平均间隔: {avg:F2}ms); Console.WriteLine($ 标准差: {std:F2}ms); Console.WriteLine($ 最大间隔: {intervals.Max():F2}ms); Console.WriteLine(); } }我在这台机器上跑出来的结果大致是这样的不同机器会有差异但趋势一致定时器平均间隔标准差最大间隔System.Timers.Timer101.3ms2.1ms118msSystem.Threading.Timer100.8ms1.8ms112msPeriodicTimer102.5ms3.4ms125ms可以看到System.Threading.Timer的精度最好PeriodicTimer因为异步状态机的开销精度略差一点。但差距都在可接受范围内。真正影响精度的是系统负载如果CPU占用率高所有定时器的波动都会变大。注意这个测试是在控制台程序里跑的没有UI线程干扰。如果在WinForms里用System.Windows.Forms.TimerUI一忙最大间隔可能飙到几百毫秒。4.2 参数计算间隔设多少才合理很多人设间隔是拍脑袋觉得“差不多就行”。但在一些场景下间隔设错了会直接导致功能异常。我举两个实际例子。例子一数据库轮询。假设你要每5秒查一次数据库看有没有新订单。如果设成1秒数据库压力大设成10秒业务延迟高。合理的做法是先估算单次查询耗时比如平均50ms那么间隔至少要是查询耗时的3~5倍也就是150~250ms以上。再结合业务对延迟的容忍度5秒是个平衡点。例子二UI进度条刷新。进度条不需要太高的刷新率人眼对超过30fps的刷新基本无感。所以间隔设在33ms以上就够了。设成10ms反而浪费UI线程资源导致其他操作卡顿。这里有一个通用的计算公式最小安全间隔 单次回调平均耗时 × 3如果回调耗时波动大还要乘以一个安全系数比如1.5。最终间隔 单次回调平均耗时 × 3 × 1.5 ≈ 单次耗时 × 4.5。4.3 资源释放Dispose的正确姿势定时器是IDisposable的但很多人不释放觉得“程序关了自然就没了”。在短生命周期的程序里确实没问题但在长时间运行的服务里不释放定时器会导致内存泄漏和线程泄漏。正确的释放姿势分三步停止定时器Stop()或Change(Timeout.Infinite, Timeout.Infinite)取消订阅事件-避免事件持有回调引用调用Dispose()public class MyService : IDisposable { private System.Timers.Timer _timer; private readonly CancellationTokenSource _cts new(); public MyService() { _timer new System.Timers.Timer(1000); _timer.Elapsed OnElapsed; _timer.Start(); } private void OnElapsed(object sender, ElapsedEventArgs e) { if (_cts.IsCancellationRequested) return; // 业务逻辑 } public void Dispose() { _cts.Cancel(); _timer.Stop(); _timer.Elapsed - OnElapsed; _timer.Dispose(); _cts.Dispose(); } }我踩过的坑有一次在Dispose里只调了Stop()没调Dispose()结果定时器对象一直被GC Root持有内存慢慢涨。后来用内存分析工具才定位到。从那以后凡是IDisposable的东西我都用using或者显式Dispose。5. 常见问题与排查技巧实录5.1 定时器不触发或触发次数不对这是最常见的问题原因通常有这几类现象可能原因排查方法解决方案完全不触发定时器没Start或Dispose了检查Start调用和Dispose调用确保Start在Dispose之前触发几次后停了回调抛异常导致线程池线程终止在回调里加try-catch捕获所有异常触发次数比预期少回调耗时超过间隔被重入保护跳过打印每次触发时间增大间隔或优化回调触发次数比预期多AutoReset为true且回调耗时短检查AutoReset设置设为false手动控制UI线程上不触发UI线程被阻塞检查是否有同步耗时操作改用async或移到后台线程我重点说一下“回调抛异常”这个坑。System.Timers.Timer的Elapsed事件如果抛异常异常会在线程池线程上抛出默认情况下会导致进程崩溃。而且定时器本身不会停它会继续触发继续抛异常。所以一定要在回调最外层包try-catch。private void OnElapsed(object sender, ElapsedEventArgs e) { try { // 业务逻辑 } catch (Exception ex) { // 记录日志不要让异常逃逸 Console.WriteLine($定时器回调异常: {ex.Message}); } }5.2 多线程重入导致的数据错乱前面提过重入问题这里展开讲一下排查方法。重入的典型表现是日志里出现两条时间戳几乎相同的记录或者集合操作抛InvalidOperationException。排查步骤在回调入口打印Thread.CurrentThread.ManagedThreadId和DateTime.Now。如果看到不同线程ID在极短时间内进入就是重入。用Interlocked.CompareExchange加互斥标志或者改用PeriodicTimer。private int _running 0; private void OnElapsed(object sender, ElapsedEventArgs e) { if (Interlocked.CompareExchange(ref _running, 1, 0) ! 0) { Console.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] 跳过重入); return; } try { Console.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] 线程{Thread.CurrentThread.ManagedThreadId}开始); Thread.Sleep(1500); // 模拟耗时操作 } finally { Interlocked.Exchange(ref _running, 0); } }5.3 程序退出时定时器还在跑这个问题在控制台程序和服务里特别常见。主线程退出了但定时器还在线程池里跑导致进程无法正常退出或者退出时抛异常。解决办法是给定时器配一个CancellationToken在程序退出时取消。private CancellationTokenSource _cts new(); private void OnElapsed(object sender, ElapsedEventArgs e) { if (_cts.IsCancellationRequested) return; // 业务逻辑 } // 程序退出时 _cts.Cancel(); _timer.Stop(); _timer.Dispose();对于PeriodicTimer直接把CancellationToken传给WaitForNextTickAsync即可取消后循环自然退出。5.4 独家避坑清单以下是我这些年踩过的坑整理成清单供你对照检查不要用System.Windows.Forms.Timer做后台任务它只适合UI刷新。不要在Elapsed事件里直接操作UI控件会抛跨线程异常。用Invoke或BeginInvoke。不要依赖定时器的绝对精度Windows不是实时系统精度受系统时钟和负载影响。不要在回调里调用Dispose会导致死锁或异常。在外部线程调用。不要忘记取消事件订阅否则定时器对象无法被GC回收。不要用Thread.Sleep在回调里模拟耗时这会占用线程池线程影响其他任务。不要设间隔小于10ms除非你确实需要高频采样否则浪费CPU。不要在PeriodicTimer里用ConfigureAwait(true)除非你明确需要回到原上下文。6. 选型决策树与实战建议6.1 一张图帮你三秒选对定时器虽然不能画图但我可以用文字描述这个决策树第一步回调需要操作UI控件吗是 → 用System.Windows.Forms.TimerWinForms或DispatcherTimerWPF否 → 进入第二步第二步回调是异步逻辑吗需要await是 → 用PeriodicTimer.NET 6否 → 进入第三步第三步需要极致性能或底层控制吗是 → 用System.Threading.Timer否 → 用System.Timers.Timer这个决策树覆盖了90%的场景。剩下的10%是一些特殊需求比如需要动态修改间隔、需要状态对象传参等那就根据具体情况选。6.2 不同场景的实战建议场景一WinForms上位机每500ms刷新一次传感器数据。用System.Windows.Forms.Timer在Tick里直接更新控件。如果读取传感器是IO操作用async void加await Task.Run但更新控件前要Invoke回来。场景二ASP.NET Core后台服务每30秒清理一次过期缓存。用PeriodicTimer配合BackgroundService的ExecuteAsync方法。代码简洁支持取消不会重入。场景三高频数据采集每5ms读一次串口。用System.Threading.Timer间隔设5ms回调里只做数据读取和入队不做耗时处理。用Interlocked做重入保护。场景四Unity游戏逻辑每帧检查一次状态。Unity有自己的Update和InvokeRepeating一般不用C#原生定时器。如果非要用用System.Threading.Timer但要注意Unity API不能在非主线程调用。6.3 一个容易忽略的细节定时器的GC影响System.Threading.Timer和System.Timers.Timer都会持有回调委托的引用如果回调里捕获了外部对象闭包这些对象就无法被GC回收。在长时间运行的服务里这会导致内存缓慢增长。解决办法是尽量用静态方法或状态对象传参避免闭包捕获。// 不好的写法闭包捕获了this _timer new System.Threading.Timer(_ this.DoWork(), null, 0, 1000); // 好的写法用状态对象 _timer new System.Threading.Timer( state ((MyService)state).DoWork(), this, 0, 1000 );但状态对象方式也有个问题它同样持有this的引用。如果服务需要被回收还是得先Dispose定时器。所以核心还是那句话用完就释放。6.4 关于PeriodicTimer的一个补充技巧PeriodicTimer的WaitForNextTickAsync在取消时会抛OperationCanceledException而不是返回false。这一点和文档里说的“返回false表示定时器已释放”容易混淆。实际行为是如果传入的CancellationToken被取消抛异常如果定时器被Dispose返回false。所以正确的循环写法是using var timer new PeriodicTimer(TimeSpan.FromSeconds(1)); try { while (await timer.WaitForNextTickAsync(ct)) { await DoWorkAsync(); } } catch (OperationCanceledException) { // 正常取消忽略 }这个细节我在第一次用的时候被坑过以为取消会返回false结果异常直接冒泡到上层导致日志里一堆错误。后来加了try-catch才干净。7. 我个人的使用体会这些年下来我的选择习惯已经比较固定了。桌面端UI刷新无脑用System.Windows.Forms.Timer或DispatcherTimer不折腾。服务端后台任务如果项目是.NET 6优先用PeriodicTimer代码干净、语义清晰、不会重入。如果是老项目或者需要极致性能用System.Threading.Timer但一定会加Interlocked保护。System.Timers.Timer现在用得少了主要是因为它夹在中间既没有UI线程的便利也没有PeriodicTimer的异步优雅唯一的好处是API比较直观适合快速原型。最后分享一个小技巧如果你不确定该用哪个先写一个最简单的System.Threading.Timer版本跑起来看看回调线程ID和触发间隔。如果发现需要操作UI再换成UI定时器如果发现需要await再换成PeriodicTimer。不要一开始就纠结选型先跑通再优化比空想快得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询