Menu

Timer C#: System.Threading.Timer, System.Timers.Timer, PeriodicTimer

C# punya beberapa class timer dan perilakunya berbeda. Pelajari kapan memakai System.Threading.Timer, System.Timers.Timer, PeriodicTimer atau timer UI, cara memulai dan menghentikannya, kenapa callback berjalan di thread pool dan bisa tumpang tindih, kenapa timer harus tetap direferensikan, dan cara mengukur waktu yang berlalu dengan Stopwatch.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Timer menjalankan kode setelah jeda tertentu atau berulang pada interval tertentu: menyegarkan cache setiap menit, menyimpan draf setiap 30 detik, memeriksa sebuah service, memperbarui jam. .NET punya beberapa class timer yang terlihat mirip, tetapi berbeda dalam hal di mana callback berjalan dan bagaimana kamu mengendalikannya. Halaman ini membahas masing-masing, ditambah Stopwatch untuk mengukur berapa lama sesuatu berjalan.

Timer mana yang dipakai

TimerCallback berjalan diGayaCocok untuk
System.Threading.TimerThread poolDelegate callbackPekerjaan latar belakang di service dan library
System.Timers.TimerThread pool (secara default)Event Elapsed, Start/StopKode bergaya komponen yang suka event
PeriodicTimer (.NET 6+)Di mana pun loop async-mu berjalanawait WaitForNextTickAsync()Kode async; tick tidak pernah tumpang tindih
System.Windows.Forms.Timer, DispatcherTimer di WPFThread UIEvent TickMemperbarui kontrol di aplikasi desktop

Dua timer pertama berjalan di thread dari thread pool, jadi callback-nya bisa berjalan bersamaan dengan bagian lain programmu, bahkan bersamaan satu sama lain. Timer UI berjalan di thread UI, karena itu timer tersebut bisa langsung menyentuh kontrol, dan karena itu juga handler yang lambat membuat jendela membeku.

System.Threading.Timer

Constructor-nya menerima callback, objek state, jeda sebelum tick pertama, dan periode. Timer langsung mulai berjalan.

Contoh output:

tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks

Ada dua detail penting di sini. Main harus menunggu (done.WaitOne()); program console berakhir saat Main selesai, dan callback timer berjalan di thread latar belakang yang tidak membuat proses tetap hidup. Selain itu, timer disimpan di field static: System.Threading.Timer yang tidak direferensikan oleh apa pun bisa dibersihkan oleh garbage collector, lalu diam-diam berhenti berjalan. Masalah ini paling sering muncul saat timer dibuat sebagai variabel lokal di dalam method yang sudah selesai.

Change(dueTime, period) menjadwalkan ulang timer; Timeout.Infinite untuk keduanya menjedanya, dan pasangan nilai baru memulainya lagi. Dispose() menghentikannya selamanya.

System.Timers.Timer

System.Timers.Timer membungkus mekanisme yang sama dalam API berbasis event: atur Interval, berlangganan ke Elapsed, lalu panggil Start() dan Stop().

Contoh output:

autosave #1
autosave #2
autosave #3
autosave stopped

AutoReset = false membuat timer sekali jalan: timer berjalan sekali lalu berhenti, dan memanggil Start() lagi menjadwalkan satu tick berikutnya. Enabled = true dan false sama dengan Start() dan Stop(). Nama lengkap System.Timers.Timer ditulis di sini karena System.Threading juga punya Timer; kalau kedua namespace diimpor, Timer saja menjadi ambigu dan tidak bisa dikompilasi.

System.Timers.Timer juga punya property SynchronizingObject yang memindahkan Elapsed ke thread UI di WinForms. Dalam praktiknya, timer milik framework UI itu sendiri lebih sederhana untuk keperluan itu.

Callback bisa tumpang tindih dan berjalan di thread lain

Kedua class di atas berjalan di thread pool sesuai jadwal, tidak peduli callback sebelumnya sudah selesai atau belum. Kalau sebuah callback butuh 3 detik dan periodenya 1 detik, tiga callback berjalan sekaligus. Ini menimbulkan dua kewajiban:

  • Apa pun yang disentuh callback harus thread safe. Pakai Interlocked untuk counter (seperti di contoh) dan lock untuk hal yang lebih besar.
  • Cegah tumpang tindih kalau itu penting. Lewati satu tick selama tick sebelumnya masih berjalan, atau pakai pola yang memang tidak bisa tumpang tindih.

Penjaga untuk melewati tick dengan Interlocked:

private int running = 0;

private void OnTick(object state)
{
    if (Interlocked.Exchange(ref running, 1) == 1) return; // previous tick still busy
    try
    {
        SyncOrders();   // slow work
    }
    finally
    {
        Volatile.Write(ref running, 0);
    }
}

Exception adalah jebakan lainnya. Exception yang lolos dari callback System.Threading.Timer membuat proses crash, sedangkan System.Timers.Timer menelan exception dari handler Elapsed sehingga kegagalannya tidak terlihat. Bungkus isi callback timer dengan try/catch dan catat ke log.

PeriodicTimer dan loop async

Di kode async, loop yang menunggu di antara iterasi adalah timer yang paling jelas: penantian berikutnya baru dimulai setelah pekerjaan selesai, jadi tick tidak pernah tumpang tindih, dan exception muncul di tempat yang bisa kamu tangkap. .NET 6 menambahkan PeriodicTimer untuk persis keperluan ini:

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

while (await timer.WaitForNextTickAsync(cancellationToken))
{
    await SaveDraftAsync();
}

WaitForNextTickAsync mengembalikan false begitu timer di-dispose, yang mengakhiri loop, dan melempar OperationCanceledException saat token dibatalkan. Berbeda dengan loop di sekitar Task.Delay, PeriodicTimer menjaga ritme yang tetap: kalau pekerjaannya butuh 2 detik, tick berikutnya tetap datang 30 detik setelah tick sebelumnya, bukan 32.

Di versi mana pun, bentuk yang sama dengan Task.Delay bisa dipakai saat pergeseran waktu tidak jadi masalah:

Contoh output:

poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds

Pembatalan menggantikan Stop(): kirim token ke dalam loop dan batalkan dari luar. Lihat task untuk penjelasan lebih lanjut tentang cancellation token.

Mengukur waktu yang berlalu dengan Stopwatch

Timer menjadwalkan kode; Stopwatch mengukur berapa lama kode berjalan. Stopwatch membaca jam beresolusi tinggi yang hanya bergerak maju, sehingga menjadi alat yang tepat untuk mengukur waktu:

Contoh output:

string +=      64 ms
StringBuilder  0 ms
Same length: True
High resolution clock: True

Start, Stop, Reset dan Restart mengendalikannya; Elapsed bertipe TimeSpan, sedangkan ElapsedMilliseconds dan ElapsedTicks memberi angka mentah. Hindari mengukur waktu dengan DateTime.Now: resolusinya bisa sekasar 10 sampai 15 ms, dan nilainya melompat saat jam sistem disesuaikan. Untuk benchmark yang serius, saat pemanasan JIT dan garbage collection mengacaukan hasil satu kali jalan, pakai library BenchmarkDotNet.

Akurasi timer

Interval timer adalah batas minimum, bukan janji. Di Windows, resolusi timer sistem default sekitar 15,6 ms, jadi interval 10 ms cenderung berjalan setiap 15 atau 16 ms, dan di mesin yang sibuk callback thread pool bisa mulai lebih lambat lagi. Timer cocok untuk "kira-kira setiap N detik". Untuk timing frame atau media yang presisi, pakai API yang dibuat untuk itu.

Kesalahan umum

  • System.Threading.Timer yang hanya disimpan di variabel lokal. Timer bisa dibersihkan garbage collector dan berhenti berjalan; simpan di field.
  • Membiarkan Main selesai. Prosesnya berakhir dan timer ikut berhenti.
  • Timer yang ambigu. Kalau System.Threading dan System.Timers sama-sama diimpor, tulis nama lengkapnya.
  • Menganggap tick tidak pernah tumpang tindih. Timer thread pool tumpang tindih saat pekerjaannya lebih lambat dari interval.
  • Menyentuh kontrol UI dari timer thread pool. Pakai timer milik framework UI atau kembalikan ke thread UI.
  • Exception yang tidak ditangani di callback. Exception itu membuat proses crash (Threading.Timer) atau lenyap begitu saja (Timers.Timer); tangkap dan catat ke log.
  • Lupa Dispose. Timer memegang resource sistem dan terus berjalan sampai di-dispose.

Pertanyaan yang sering diajukan

Timer mana yang sebaiknya dipakai di C#?

Di kode async pada .NET 6 atau sesudahnya, pakai PeriodicTimer dengan await timer.WaitForNextTickAsync() di dalam loop. Untuk callback di thread pool, pakai System.Threading.Timer. Untuk timer berbasis event dengan Start, Stop dan event Elapsed, pakai System.Timers.Timer. Di WinForms atau WPF, pakai timer milik framework UI agar handler berjalan di thread UI.

Bagaimana cara menjalankan kode setiap beberapa detik di C#?

Buat timer dengan interval yang diinginkan, misalnya new System.Threading.Timer(_ => Refresh(), null, 0, 5000) untuk memanggil Refresh sekarang dan setiap 5 detik, lalu simpan referensinya. Di kode async, buat loop dengan PeriodicTimer (.NET 6+) atau await Task.Delay(5000); versi loop tidak pernah menjalankan dua tick sekaligus.

Bagaimana cara menghentikan timer di C#?

Untuk System.Timers.Timer, panggil Stop() (atau atur Enabled = false), lalu Dispose() saat kamu tidak membutuhkannya lagi. Untuk System.Threading.Timer, panggil Change(Timeout.Infinite, Timeout.Infinite) untuk menjedanya, atau Dispose() untuk menghentikannya selamanya. Callback yang sudah mulai berjalan mungkin tetap selesai setelah timer dihentikan.

Kenapa System.Threading.Timer saya berhenti berjalan?

Tidak ada yang mereferensikan timer itu, jadi garbage collector membersihkannya dan callback-nya berhenti. Ini terjadi saat timer dibuat sebagai variabel lokal di method yang sudah selesai. Simpan timer di field selama ia masih perlu berjalan.

Bagaimana cara mengukur waktu yang berlalu di C#?

Pakai System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); lalu baca sw.ElapsedMilliseconds atau sw.Elapsed. Stopwatch memakai jam monotonik beresolusi tinggi, berbeda dengan mengurangkan dua nilai DateTime.Now, yang resolusinya lebih kasar dan bisa melompat saat jam sistem disesuaikan.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai