Bir timer kodu bir gecikmeden sonra ya da yinelenen bir aralıkla çalıştırır: bir önbelleği her dakika yenilemek, bir taslağı her 30 saniyede kaydetmek, bir servisi yoklamak, bir saati güncellemek. .NET'te birbirine benzeyen ama callback'in nerede çalıştığı ve onları nasıl kontrol ettiğiniz bakımından farklılaşan birkaç timer sınıfı vardır. Bu sayfa her birini ve bir şeyin ne kadar sürdüğünü ölçmek için Stopwatch'ı ele alır.
Hangi timer kullanılmalı
| Timer | Callback'in çalıştığı yer | Stil | Kullanım |
|---|---|---|---|
System.Threading.Timer | Thread pool | Callback delegate'i | Servislerde ve kütüphanelerde arka plan işi |
System.Timers.Timer | Thread pool (varsayılan olarak) | Elapsed event'i, Start/Stop | Event'leri seven bileşen tarzı kod |
PeriodicTimer (.NET 6+) | Async döngünüz nerede çalışıyorsa | await WaitForNextTickAsync() | Async kod; tick'ler asla üst üste binmez |
System.Windows.Forms.Timer, WPF DispatcherTimer | UI thread'i | Tick event'i | Masaüstü uygulamalarında kontrolleri güncellemek |
İlk ikisi thread pool thread'lerinde tetiklenir, bu yüzden callback'leri programınızın geri kalanıyla aynı anda, hatta birbirleriyle aynı anda çalışabilir. UI timer'ları UI thread'inde çalışır; kontrollere doğrudan dokunabilmelerinin ve yavaş bir handler'ın pencereyi dondurmasının nedeni budur.
System.Threading.Timer
Constructor callback'i, bir durum nesnesini, ilk tick'ten önceki gecikmeyi ve periyodu alır. Hemen başlar.
Örnek çıktı:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Burada iki ayrıntı önemlidir. Main beklemek zorundadır (done.WaitOne()); bir konsol programı Main döndüğünde çıkar ve timer callback'leri süreci canlı tutmayan arka plan thread'lerinde çalışır. Ve timer statik bir alanda yaşar: hiçbir şeyin referans vermediği bir System.Threading.Timer çöp toplayıcı tarafından toplanabilir ve sonra sessizce tetiklenmeyi bırakır. Bu en çok, bir timer dönen bir metodun içinde yerel bir değişken olarak oluşturulduğunda ısırır.
Change(dueTime, period) timer'ı yeniden planlar; ikisi için de Timeout.Infinite onu duraklatır ve yeni bir çift onu yeniden başlatır. Dispose() onu kalıcı olarak durdurur.
System.Timers.Timer
System.Timers.Timer aynı mekanizmayı event tabanlı bir API'ye sarar: Interval'ı ayarlayın, Elapsed'a abone olun, sonra Start() ve Stop().
Örnek çıktı:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false tek seferlik bir timer yapar: bir kez tetiklenir ve durur, Start()'ı tekrar çağırmak sonraki tek tick'i planlar. Enabled = true ve false, Start() ve Stop() ile aynıdır. System.Threading'de de bir Timer olduğu için burada tam ad System.Timers.Timer yazılmıştır; iki namespace de içe aktarılmışken çıplak bir Timer belirsizdir ve derlenmez.
System.Timers.Timer'ın ayrıca WinForms'ta Elapsed'ı bir UI thread'ine aktaran bir SynchronizingObject property'si vardır. Uygulamada bunun için UI framework'ünün kendi timer'ı daha basittir.
Callback'ler üst üste biner ve başka thread'lerde çalışır
Yukarıdaki iki sınıf da, önceki callback bitmiş olsun ya da olmasın, planlandığı gibi thread pool'da tetiklenir. Bir callback 3 saniye sürüyorsa ve periyot 1 saniyeyse, üç callback aynı anda çalışır. Bu iki yükümlülük doğurur:
- Callback'in dokunduğu her şey thread-safe olmalıdır. Sayaçlar için (örneklerin yaptığı gibi)
Interlocked, daha büyük her şey için bir lock kullanın. - Önemli olduğunda üst üste binmeye karşı koruma alın. Ya önceki hâlâ çalışırken bir tick'i atlayın ya da üst üste binemeyen bir kalıp kullanın.
Interlocked ile bir atlama koruması:
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);
}
}
İstisnalar diğer tuzaktır. Bir System.Threading.Timer callback'inden kaçan bir istisna süreci çökertir, System.Timers.Timer ise Elapsed handler'larından gelen istisnaları yutar ve başarısızlık fark edilmez. Bir timer callback'inin gövdesini try/catch içine alın ve log'a yazın.
PeriodicTimer ve async döngüler
Async kodda, yinelemeler arasında bekleyen bir döngü en anlaşılır timer'dır: sonraki bekleme ancak iş bittikten sonra başlar, bu yüzden tick'ler asla üst üste binemez ve istisnalar yakalayabileceğiniz yerde ortaya çıkar. .NET 6 tam olarak bunun için PeriodicTimer'ı ekledi:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync, timer dispose edildikten sonra döngüyü bitiren false döndürür ve token iptal edildiğinde OperationCanceledException fırlatır. Task.Delay etrafındaki bir döngünün aksine bir PeriodicTimer sabit bir ritim korur: iş 2 saniye sürerse sonraki tick yine önceki tick'ten 32 değil 30 saniye sonra gelir.
Kaymanın önemli olmadığı durumlarda her sürümde Task.Delay ile aynı şekil çalışır:
Örnek çıktı:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
İptal Stop()'un yerini alır: döngüye bir token verin ve onu dışarıdan iptal edin. İptal token'ları hakkında daha fazlası için task'lar sayfasına bakın.
Stopwatch ile geçen süreyi ölçmek
Bir timer kodu planlar; Stopwatch kodun ne kadar sürdüğünü ölçer. Yalnızca ileri giden yüksek çözünürlüklü bir saati okur; bu da onu süre ölçmek için doğru araç yapar:
Örnek çıktı:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Start, Stop, Reset ve Restart onu kontrol eder; Elapsed bir TimeSpan'dir, ElapsedMilliseconds ve ElapsedTicks ham sayılar verir. DateTime.Now ile süre ölçmekten kaçının: çözünürlüğü 10 ile 15 ms kadar kaba olabilir ve sistem saati ayarlandığında sıçrar. JIT ısınmasının ve çöp toplamanın tek bir çalıştırmayı bozduğu ciddi benchmark'lar için BenchmarkDotNet kütüphanesini kullanın.
Timer doğruluğu
Bir timer aralığı bir vaat değil, bir alt sınırdır. Windows'ta varsayılan sistem timer çözünürlüğü yaklaşık 15,6 ms'dir, bu yüzden 10 ms'lik bir aralık her 15 ya da 16 ms'de bir tetiklenme eğilimindedir ve meşgul bir makinede bir thread pool callback'i daha da geç başlayabilir. Timer'lar "yaklaşık her N saniyede bir" için doğrudur. Kesin kare zamanlaması ya da medya için bunun için yapılmış API'leri kullanın.
Yaygın hatalar
- Yalnızca yerel bir değişkende saklanan bir
System.Threading.Timer. Çöp toplayıcı tarafından toplanıp tetiklenmeyi bırakabilir; onu bir alanda tutun. Main'in dönmesine izin vermek. Süreç biter ve timer'ı da beraberinde götürür.- Belirsiz
Timer.System.ThreadingveSystem.Timersikisi de içe aktarılmışken tam adı yazın. - Tick'lerin asla üst üste binmediğini varsaymak. İş aralıktan yavaş olduğunda thread pool timer'ları üst üste biner.
- Bir thread pool timer'ından UI kontrollerine dokunmak. UI framework'ünün timer'ını kullanın ya da UI thread'ine geri aktarın.
- Callback'lerde ele alınmayan istisnalar. Süreci çökertirler (
Threading.Timer) ya da kaybolurlar (Timers.Timer); yakalayıp log'a yazın. Dispose'u unutmak. Timer'lar sistem kaynakları tutar ve dispose edilene kadar tetiklenmeye devam eder.
Sıkça Sorulan Sorular
C#'ta hangi timer'ı kullanmalıyım?
.NET 6 ya da sonrasında async kodda, bir döngüde await timer.WaitForNextTickAsync() ile PeriodicTimer. Thread pool'da bir callback için System.Threading.Timer. Start, Stop ve bir Elapsed event'ine sahip event tabanlı bir timer için System.Timers.Timer. WinForms ya da WPF'te handler'ın UI thread'inde çalışması için UI framework'ünün timer'ını kullanın.
C#'ta her birkaç saniyede bir nasıl kod çalıştırılır?
Aralıkla bir timer oluşturun, örneğin Refresh'i şimdi ve her 5 saniyede bir çağırmak için new System.Threading.Timer(_ => Refresh(), null, 0, 5000), ve ona bir referans tutun. Async kodda PeriodicTimer (.NET 6+) ya da await Task.Delay(5000) ile döngü kurun; döngü sürümü asla iki tick'i aynı anda çalıştırmaz.
C#'ta bir timer nasıl durdurulur?
System.Timers.Timer için Stop() çağırın (ya da Enabled = false yapın) ve artık ihtiyacınız olmadığında Dispose() çağırın. System.Threading.Timer için duraklatmak üzere Change(Timeout.Infinite, Timeout.Infinite), kalıcı olarak durdurmak için Dispose() çağırın. Zaten başlamış bir callback, timer'ı durdurduktan sonra da bitebilir.
System.Threading.Timer'ım neden tetiklenmeyi bırakıyor?
Timer'a hiçbir şey referans vermiyor, bu yüzden çöp toplayıcı onu topladı ve callback'leri durdu. Bu, timer dönen bir metotta yerel bir değişken olarak oluşturulduğunda olur. Çalışmaya devam etmesi gerektiği sürece onu bir alanda saklayın.
C#'ta geçen süre nasıl ölçülür?
System.Diagnostics.Stopwatch kullanın: var sw = Stopwatch.StartNew(); ... sw.Stop();, sonra sw.ElapsedMilliseconds'ı ya da sw.Elapsed'ı okuyun. Daha kaba çözünürlüğe sahip olan ve sistem saati ayarlandığında sıçrayan iki DateTime.Now değerini çıkarmanın aksine yüksek çözünürlüklü, monoton bir saat kullanır.