Menu

Timer ב-C#: System.Threading.Timer, System.Timers.Timer ו-PeriodicTimer

ל-C# יש כמה מחלקות טיימר והן מתנהגות אחרת. למדו מתי להשתמש ב-System.Threading.Timer, ב-System.Timers.Timer, ב-PeriodicTimer או בטיימר של ממשק משתמש, איך מפעילים ועוצרים אותם, למה callbacks רצים על מאגר התהליכונים ויכולים לחפוף, למה חייבים לשמור הפניה לטיימר, ואיך מודדים זמן שעבר עם Stopwatch.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

טיימר מריץ קוד אחרי השהיה או במרווח חוזר: רענון מטמון כל דקה, שמירת טיוטה כל 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, שהרזולוציה שלו גסה יותר והוא קופץ כשמכוונים את שעון המערכת.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל