טיימר מריץ קוד אחרי השהיה או במרווח חוזר: רענון מטמון כל דקה, שמירת טיוטה כל 30 שניות, תשאול של שירות, עדכון שעון. ל-.NET יש כמה מחלקות טיימר שנראות דומות אבל שונות במקום שבו ה-callback רץ ובדרך שבה שולטים בהן. העמוד הזה מכסה כל אחת מהן, ובנוסף את Stopwatch למדידת משך הזמן שמשהו לקח.
באיזה טיימר להשתמש
| טיימר | ה-callback רץ על | סגנון | מתאים ל |
|---|---|---|---|
System.Threading.Timer | מאגר התהליכונים | delegate של callback | עבודת רקע בשירותים ובספריות |
System.Timers.Timer | מאגר התהליכונים (כברירת מחדל) | אירוע Elapsed, Start/Stop | קוד בסגנון רכיבים שאוהב אירועים |
PeriodicTimer (.NET 6+) | היכן שהלולאה האסינכרונית שלכם רצה | await WaitForNextTickAsync() | קוד אסינכרוני; ticks לעולם לא חופפים |
System.Windows.Forms.Timer, DispatcherTimer של WPF | תהליכון הממשק | אירוע Tick | עדכון פקדים באפליקציות שולחן עבודה |
שני הראשונים פועלים על תהליכונים ממאגר התהליכונים, כך שה-callbacks שלהם יכולים לרוץ באותו זמן כמו שאר התוכנית, ואפילו באותו זמן זה כמו זה. טיימרי הממשק רצים על תהליכון הממשק, ולכן הם יכולים לגעת בפקדים ישירות, ולכן גם מטפל איטי מקפיא את החלון.
System.Threading.Timer
הבנאי מקבל את ה-callback, אובייקט מצב, את ההשהיה לפני ה-tick הראשון ואת המחזור. הוא מתחיל מיד.
פלט לדוגמה:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
שני פרטים חשובים כאן. Main חייבת לחכות (done.WaitOne()); תוכנית קונסולה מסתיימת כש-Main חוזרת, ו-callbacks של טיימר רצים על תהליכוני רקע שלא משאירים את התהליך בחיים. והטיימר חי בשדה סטטי: System.Threading.Timer ששום דבר לא מפנה אליו יכול להיאסף על ידי אוסף הזבל, ואז הוא מפסיק לפעול בשקט. זה קורה בעיקר כשטיימר נוצר כמשתנה מקומי בתוך מתודה שחוזרת.
Change(dueTime, period) מתזמנת את הטיימר מחדש; Timeout.Infinite בשניהם משהה אותו, וזוג ערכים חדש מפעיל אותו שוב. Dispose() עוצרת אותו לתמיד.
System.Timers.Timer
System.Timers.Timer עוטף את אותו מנגנון ב-API מבוסס אירועים: מגדירים Interval, נרשמים ל-Elapsed, ואז Start() ו-Stop().
פלט לדוגמה:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false יוצר טיימר חד-פעמי: הוא פועל פעם אחת ונעצר, וקריאה חוזרת ל-Start() מתזמנת את ה-tick הבודד הבא. Enabled = true ו-false זהים ל-Start() ול-Stop(). השם המלא System.Timers.Timer כתוב כאן כי גם ב-System.Threading יש Timer; כששני מרחבי השמות מיובאים, Timer לבד הוא דו-משמעי ולא מתקמפל.
ל-System.Timers.Timer יש גם מאפיין SynchronizingObject שמעביר את Elapsed לתהליכון ממשק ב-WinForms. בפועל, הטיימר של ספריית הממשק עצמה פשוט יותר לשם כך.
callbacks חופפים ורצים על תהליכונים אחרים
שתי המחלקות שלמעלה פועלות על מאגר התהליכונים, בזמן, בין אם ה-callback הקודם הסתיים ובין אם לא. אם callback לוקח 3 שניות והמחזור הוא שנייה אחת, שלושה callbacks רצים בבת אחת. זה יוצר שתי חובות:
- כל מה שה-callback נוגע בו חייב להיות בטוח לתהליכונים. השתמשו ב-
Interlockedלמונים (כמו בדוגמאות) וב-lock לכל דבר גדול יותר. - הגנו מפני חפיפה כשזה חשוב. או שמדלגים על tick בזמן שהקודם עדיין רץ, או שמשתמשים בדפוס שלא יכול לחפוף.
הגנת דילוג עם 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);
}
}
חריגות הן המלכודת השנייה. חריגה שבורחת מ-callback של System.Threading.Timer מפילה את התהליך, בעוד ש-System.Timers.Timer בולע חריגות ממטפלי Elapsed כך שהכישלון עובר בלי שמישהו ישים לב. עטפו את הגוף של callback של טיימר ב-try/catch ורשמו ללוג.
PeriodicTimer ולולאות אסינכרוניות
בקוד אסינכרוני, לולאה שמחכה בין איטרציות היא הטיימר הברור ביותר: ההמתנה הבאה מתחילה רק אחרי שהעבודה הסתיימה, כך ש-ticks לעולם לא חופפים, וחריגות צצות במקום שבו אפשר לתפוס אותן. .NET 6 הוסיפה את PeriodicTimer בדיוק לשם כך:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync מחזירה false ברגע שהטיימר משוחרר, וזה מסיים את הלולאה, וזורקת OperationCanceledException כשה-token מבוטל. בניגוד ללולאה סביב Task.Delay, PeriodicTimer שומר על קצב קבוע: אם העבודה לוקחת 2 שניות, ה-tick הבא עדיין מגיע 30 שניות אחרי ה-tick הקודם, לא 32.
בכל גרסה, אותה צורה עם Task.Delay עובדת כשסטייה בזמן לא משנה:
פלט לדוגמה:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
ביטול מחליף את Stop(): מעבירים token ללולאה ומבטלים אותו מבחוץ. ראו tasks להרחבה על cancellation tokens.
מדידת זמן שעבר עם Stopwatch
טיימר מתזמן קוד; Stopwatch מודד כמה זמן קוד לקח. הוא קורא שעון ברזולוציה גבוהה שנע רק קדימה, ולכן הוא הכלי הנכון למדידת זמנים:
פלט לדוגמה:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Start, Stop, Reset ו-Restart שולטים בו; Elapsed הוא TimeSpan, ו-ElapsedMilliseconds ו-ElapsedTicks נותנים מספרים גולמיים. הימנעו ממדידת זמן עם DateTime.Now: הרזולוציה שלו יכולה להיות גסה עד 10 עד 15 ms, והוא קופץ כשמכוונים את שעון המערכת. לבנצ'מרקים רציניים, שבהם חימום ה-JIT ואיסוף הזבל מעוותים הרצה בודדת, השתמשו בספרייה BenchmarkDotNet.
דיוק של טיימרים
מרווח של טיימר הוא מינימום, לא הבטחה. ב-Windows רזולוציית ברירת המחדל של טיימר המערכת היא בערך 15.6 ms, כך שמרווח של 10 ms נוטה לפעול כל 15 או 16 ms, ובמחשב עמוס callback ממאגר התהליכונים יכול להתחיל אפילו מאוחר יותר. טיימרים מתאימים ל"בערך כל N שניות". לתזמון פריימים מדויק או למדיה, השתמשו ב-APIs שנבנו לשם כך.
טעויות נפוצות
System.Threading.Timerשנשמר רק במשתנה מקומי. הוא יכול להיאסף על ידי אוסף הזבל ולהפסיק לפעול; שמרו אותו בשדה.- לתת ל-
Mainלחזור. התהליך מסתיים ולוקח איתו את הטיימר. Timerדו-משמעי. כש-System.ThreadingוגםSystem.Timersמיובאים, כתבו את השם המלא.- הנחה ש-ticks לעולם לא חופפים. טיימרים של מאגר התהליכונים חופפים כשהעבודה איטית מהמרווח.
- נגיעה בפקדי ממשק מטיימר של מאגר התהליכונים. השתמשו בטיימר של ספריית הממשק או העבירו את הקריאה חזרה לתהליכון הממשק.
- חריגות שלא טופלו ב-callbacks. הן מפילות את התהליך (
Threading.Timer) או נעלמות (Timers.Timer); תפסו ורשמו ללוג. - שכחה של
Dispose. טיימרים מחזיקים משאבי מערכת וממשיכים לפעול עד שמשחררים אותם.
שאלות נפוצות
באיזה טיימר כדאי להשתמש ב-C#?
בקוד אסינכרוני ב-.NET 6 ומעלה, PeriodicTimer עם await timer.WaitForNextTickAsync() בתוך לולאה. ל-callback על מאגר התהליכונים, System.Threading.Timer. לטיימר מבוסס אירועים עם Start, Stop ואירוע Elapsed, System.Timers.Timer. ב-WinForms או ב-WPF, השתמשו בטיימר של ספריית הממשק כדי שהמטפל ירוץ על תהליכון הממשק.
איך מריצים קוד כל כמה שניות ב-C#?
צרו טיימר עם המרווח, למשל new System.Threading.Timer(_ => Refresh(), null, 0, 5000) כדי לקרוא ל-Refresh עכשיו וכל 5 שניות, ושמרו הפניה אליו. בקוד אסינכרוני, הריצו לולאה עם PeriodicTimer (.NET 6+) או await Task.Delay(5000); גרסת הלולאה לעולם לא מריצה שני ticks בבת אחת.
איך עוצרים טיימר ב-C#?
ב-System.Timers.Timer, קראו ל-Stop() (או הגדירו Enabled = false), ול-Dispose() כשכבר לא צריכים אותו. ב-System.Threading.Timer, קראו ל-Change(Timeout.Infinite, Timeout.Infinite) כדי להשהות אותו, או ל-Dispose() כדי לעצור אותו לתמיד. callback שכבר התחיל עשוי להסתיים גם אחרי שעצרתם את הטיימר.
למה ה-System.Threading.Timer שלי מפסיק לפעול?
שום דבר לא מפנה לטיימר, ולכן אוסף הזבל אסף אותו וה-callbacks שלו הפסיקו. זה קורה כשהטיימר נוצר כמשתנה מקומי במתודה שחוזרת. שמרו אותו בשדה כל עוד הוא צריך להמשיך לרוץ.
איך מודדים זמן שעבר ב-C#?
השתמשו ב-System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); ואז קראו את sw.ElapsedMilliseconds או את sw.Elapsed. הוא משתמש בשעון מונוטוני ברזולוציה גבוהה, בניגוד לחיסור של שני ערכי DateTime.Now, שהרזולוציה שלו גסה יותר והוא קופץ כשמכוונים את שעון המערכת.