首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# Timer 深度解析:三大計時器選型指南與垃圾回收陷阱完整破解

     編輯:關於C語言

.NET 三大 Timer 總覽

類別命名空間底層機制精度適用場景缺點
System.Windows.Forms.Timer System.Windows.Forms Windows 訊息迴圈 (WM_TIMER) 低 (~15-55ms) WinForms 簡單 UI 定時更新 需 UI 執行緒、無法用於 Console/Service、精度差
System.Timers.Timer System.Timers ThreadPool (I/O 完成通知) 中 (~10-15ms) Server 端定時任務、WinForms/WPF (需手動 Marshal) 事件在 ThreadPool 執行、需自行同步 UI
System.Threading.Timer System.Threading ThreadPool (輕量級) 中 (~10-15ms) 高頻背景工作、無 UI 環境、ASP.NET Core 無事件模型、需手動處理重入、同步

核心差異速查

  • System.Timers.Timer:元件化設計,支援 AutoReset、Enabled、Elapsed 事件,適合拖拉式設計(雖然實際不支援設計工具拖拉)。
  • System.Threading.Timer:純程式碼建構,單次或週期回調,無事件包裝,輕量、無 IDisposable 鏈結問題。
  • 兩者同源:System.Timers.Timer 內部封裝 System.Threading.Timer,額外提供 SynchronizingObject 自動封送至 UI 執行緒。

System.Timers.Timer 標準用法


using System.Timers;

var timer = new Timer(10000) // 10 秒間隔
{
    AutoReset = true,        // true=週期,false=單次
    Enabled = true           // 啟動
};

timer.Elapsed += (sender, e) => 
{
    Console.WriteLine($"觸發時間:{e.SignalTime:HH:mm:ss.fff}");
    // 注意:此處在 ThreadPool 執行緒,若操作 UI 必須 Invoke
};

Console.ReadLine(); // 保持程式運行
timer.Dispose();    // 釋放資源

System.Threading.Timer 標準用法


using System.Threading;

Timer timer = null;
timer = new Timer(
    callback: state => 
    {
        Console.WriteLine($"回調執行:{DateTime.Now:HH:mm:ss.fff}");
        // 單次執行後自行銷毀
        timer?.Dispose(); 
    },
    state: null,      // 傳遞給回調的狀態物件
    dueTime: 0,       // 立即啟動
    period: 2000      // 每 2 秒週期 (Timeout.Infinite 為單次)
);

Console.ReadLine();

致命陷阱:Release 模式下 Timer 被 GC 回收

現象重現


class Program
{
    static void Main()
    {
        var timer = new Timer(_ => Console.WriteLine("Tick"), null, 0, 2000);
        Console.ReadLine(); // Debug 下正常,Release 下只印一次!
    }
}

根因分析:IL 差異揭秘

模式IL 關鍵差異結果
Debug stloc.0 將 timer 存入區域變數槽 0 JIT 延長區域變數壽命至方法結束,GC 看見引用不回收
Release pop 直接丟棄 newobj 回傳參考 timer 變數無實際引用,GC 判定可回收,下次掃描即銷毀

核心原則: Timer 內部不持有對自身的強引用,若外部變數在優化後不再被讀取,GC 視為垃圾。

四種正確保活姿勢


// 1. 類別層級欄位 (最推薦,生命週期清晰)
private static Timer _timer;

// 2. 使用 GC.KeepAlive (明確告知 JIT 別優化掉)
var timer = new Timer(...);
Console.ReadLine();
GC.KeepAlive(timer); // 放在方法末尾

// 3. 存入靜態集合 (適用動態建立大量 Timer)
static readonly List _timers = new();
_timers.Add(timer);
// 移除時 _timers.Remove(timer); timer.Dispose();

// 4. 實作 IDisposable 容器統一管理
public class TimerManager : IDisposable
{
    private readonly List _timers = new();
    public Timer Create(...) { var t = new Timer(...); _timers.Add(t); return t; }
    public void Dispose() => _timers.ForEach(t => t.Dispose());
}

進階:避免重入與同步問題


var timer = new Timer(1000) { AutoReset = true };
bool _running = false;

timer.Elapsed += (s, e) => 
{
    // 雙重檢查鎖防重入
    if (Interlocked.Exchange(ref _running, 1) == 1) return;
    try 
    {
        DoWork(); // 耗時操作
    }
    finally 
    {
        Volatile.Write(ref _running, 0);
    }
};

現代替代方案:Task + Delay 循環


public async Task StartPeriodicAsync(Action work, TimeSpan interval, CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        var sw = Stopwatch.StartNew();
        work(); // 或 await workAsync()
        sw.Stop();
        
        var delay = interval - sw.Elapsed;
        if (delay > TimeSpan.Zero)
            await Task.Delay(delay, ct);
    }
}

// 使用
var cts = new CancellationTokenSource();
await StartPeriodicAsync(() => Console.WriteLine("Tick"), TimeSpan.FromSeconds(2), cts.Token);
// 隨時可取消:cts.Cancel();

優勢: 自然支援非同步、取消、例外傳播、無 GC 陷阱、易於單元測試。

總結選型建議

場景推薦方案
WinForms/WPF 簡單 UI 定時System.Windows.Forms.Timer / DispatcherTimer
後端服務定時作業System.Threading.Timer 或 IHostedService + Task.Delay
需自動 UI 封送的跨平台System.Timers.Timer (設定 SynchronizingObject)
高精度、複雜排程Quartz.NET / Hangfire / Cronos
現代 .NET 非同步週期任務Task.Delay 循環 + CancellationToken

記住:Timer 必須被強引用持有,否則 Release 模式下會神秘消失。優先考慮 Task.Delay 循環,它更安全、更現代、更符合非同步程式設計範式。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved