שלושה סוגי טיימרים ב-‏.NET — מתי להשתמש ב-PeriodicTimer/‏Timer/‏DispatcherTimer

· עודכן בתאריך: · · C#, .NET, WPF, טיימר, תכנון

במאמר הקודם, מדריך מעשי למקסום soft real-time ב-Windows רגיל — רשימת בדיקה ראשונה, סידרנו את הרעיון של הימנעות מלולאה מחזורית שמסתמכת על Sleep, ושימוש בגישה מונחית-אירועים או ב-waitable timer. במשפט אחד, המסקנה של המאמר הקודם הייתה: אם רוצים לצמצם תנודתיות במחזור ופספוסי deadline, קודם מתכננים את עצם “אופן ההמתנה”, לפני סוג הטיימר.

אז מה עושים בפיתוח אפליקציות .NET רגיל יותר? כאן קל להתבלבל בין PeriodicTimer,‏ System.Threading.Timer ו-DispatcherTimer.

כולם נקראים “טיימר”, אבל:

  • טיימר שממתין ל-tick עם await
  • טיימר שה-callback שלו מגיע מ-ThreadPool
  • טיימר שרץ על ה-Dispatcher של ת’רד ה-UI

כלומר, האופי שלהם שונה מאוד.

שלושה טיימרים עם שמות דומים אך אופי שונהתרשים המראה ש-PeriodicTimer הוא טיימר שממתין ל-tick עם await, System.Threading.Timer הוא טיימר שה-callback שלו מגיע מ-ThreadPool, ו-DispatcherTimer הוא טיימר שרץ בת'רד ה-UI.שלושה טיימריםPeriodicTimer: ממתין עם awaitTimer: ה-callback מגיעDispatcherTimer: ת'רד ה-UI

איור 1: כולם נקראים טיימר, אבל אופן ההמתנה ומקום הריצה שונים לגמרי.

בעבודה בפועל, מה שקל להתבלבל בו הוא בערך אלה:

  • מעבירים lambda של async ל-System.Threading.Timer, למרות שזה עיבוד תקופתי אסינכרוני
  • נוגעים ישירות במסך מטיימר ThreadPool, למרות שזה עדכון UI ב-WPF
  • מכניסים עיבוד כבד ל-DispatcherTimer, ומאיטים גם את המסך
  • הדיון על “soft real-time” מהמאמר הקודם מתערבב בראש עם הרצה תקופתית רגילה של אפליקציה

המאמר הזה מניח בעיקר אפליקציות C# /‏ .NET כלליות מ-‏.NET 6 ואילך, ומסדר את PeriodicTimer /‏ System.Threading.Timer /‏ DispatcherTimer בסדר שמקל על ההחלטה בעבודה היומיומית.

הקהל היעד המשוער הוא בערך אלה:

  • worker / שירות רקע
  • אפליקציית קונסולה
  • עיבוד מאחורי הקלעים ב-ASP.NET Core
  • אפליקציית desktop של WPF

DispatcherTimer במאמר הזה מתייחס בעיקר ל-System.Windows.Threading.DispatcherTimer של WPF. גם ב-WinUI /‏ UWP קיים DispatcherTimer עם אותו רעיון.

ב-WinForms, טבעי יותר להסתכל על System.Windows.Forms.Timer בתור טיימר ה-UI. המאמר הזה מטה את ההסבר בצד ה-UI ל-WPF, אבל גם קוראי WinForms נכללים בקהל היעד. אם קוראים DispatcherTimer כ-System.Windows.Forms.Timer, ההערות בסעיפים 4.3 ו-5.2 חלות כמו שהן. עם זאת, יש שני הבדלים:

  • System.Windows.Forms.Timer הוא טיימר חד-ת’רדי שה-Tick שלו קורה דרך לולאת ההודעות, ואין לו ציון עדיפות (DispatcherPriority) כמו ל-DispatcherTimer
  • בתיעוד של Microsoft נקבע שהדיוק מוגבל לכ-55 מילישניות. הוא לא מתאים למחזורים דקים — במקרה כזה כדאי לשקול טיימר שאינו טיימר UI

יש לציין: הנושא כאן הוא איך כותבים הרצה תקופתית בצד האפליקציה. כשהדיוק המחזורי עצמו הוא הנושא, חוזרים לדיון במאמר הקודם על soft real-time.

תחום הכיסוי של המאמר הזהתרשים המראה שאיך כותבים הרצה תקופתית בצד האפליקציה הוא תחום המאמר הזה, ואילו כשהדיוק המחזורי עצמו הוא הנושא חוזרים למאמר הקודם על soft real-time שעסק בתכנון אופן ההמתנה.איך כותבים הרצה תקופתית באפליקציההדיוק המחזורי עצמומה הנושא?דיון שלושת הטיימרים במאמר הזההדיון הקודם על תכנון אופן ההמתנה

איור 2: גם אם זה “לעשות משהו במרווח קבוע” — אופן כתיבת ההרצה התקופתית ותכנון דיוק המחזור הם בעיות שונות.

כמו כן, הקוד שמופיע במאמר הזה פורסם ב-GitHub כחבילת דוגמאות מלאה שאפשר לבנות ולהריץ (ספרייה ודמו קונסולה של PeriodicTimer /‏ System.Threading.Timer, ובדיקות יחידה שבודקות את קיפול ה-tick ואת חפיפת ה-callback).

periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)

תוכן עניינים

  1. קודם כול, המסקנה (במשפט אחד)
  2. קודם כול, לסדר בדף אחד
    • 2.1. התמונה הכוללת
    • 2.2. טבלת ההחלטה הראשונה
  3. מה כדאי להבחין בו קודם
    • 3.1. סוג callback, או סוג שממתין ל-tick
    • 3.2. רץ ב-ThreadPool, או בת’רד ה-UI
    • 3.3. עיבוד מחזורי והבטחת דיוק הם נושאים שונים
  4. דפוסים אופייניים
    • 4.1. לעיבוד תקופתי אסינכרוני — PeriodicTimer
    • 4.2. להרצת callback קל ב-ThreadPool — System.Threading.Timer
    • 4.3. לעדכון UI ב-WPF — DispatcherTimer
    • 4.4. לעיבוד מחזורי בכיוון soft real-time — כלי אחר
  5. אנטי-דפוסים נפוצים
  6. רשימת בדיקה לסקירת קוד
  7. חלוקה גסה בין המצבים
  8. סיכום
  9. מקורות

מפת הידע של המאמר

המאמר הזה מסדר את שלושת סוגי הטיימרים של ‏.NET לפי המקום שבו הם פועלים ולפי אופן כתיבת העיבוד. PeriodicTimer הוא טיימר מטיפוס שמשתמש ב-CancellationToken וממתין ל-tick עם await, ומתאים לעיבוד תדיר אסינכרוני ב-worker/‏BackgroundService, אבל הוא לא כלי להרצה מחזורית מדויקת מאוד. ‏System.Threading.Timer הוא טיימר קליל שה-callback שלו נקרא על ThreadPool, ומכיוון שהוא לא ממתין לסיום ה-callback הקודם עלולה להתרחש חפיפה, ואם מעבירים ל-TimerCallback שהוא void למבדה (lambda) מסוג async, זה מתנהג כמו async void, וחריגה לא מטופלת על ThreadPool עלולה להפיל את התהליך. ‏DispatcherTimer מעובד על ה-Dispatcher של WPF ולכן יכול לעדכן את ה-UI ישירות, אבל עיבוד כבד גורם לחסימת ת’רד ה-UI, וזלזול ב-Stop() או בביטול הרשמה עלול למשוך את חיי האובייקט. גם ה-Timer של WinForms חולק את המגבלה שהדיוק מוגבל לכ-55 מילישניות.

מפת הידע של הבחירה בין PeriodicTimer, ‏System.Threading.Timer ו-DispatcherTimerתרשים שמראה ש-PeriodicTimer, באמצעות CancellationToken, מתאים לעיבוד תדיר אסינכרוני ב-BackgroundService; ש-System.Threading.Timer, על ThreadPool, נושא סיכון של חפיפת callback ומלכודת בסגנון async void; ש-DispatcherTimer, על ה-Dispatcher של WPF, מוביל עדכוני UI בתמורה לסיכון של חסימת ת'רד ה-UI ושל שכחת ביטול הרשמה; ושאף אחד מהם לא מתאים לדרישה של הרצה מחזורית מדויקת מאוד.משתמש במענה מומלץ למענה מומלץ לשימוש לא מומלץ למשתמש בעלול לגרום לעלול לגרום לשימוש לא מומלץ לשימוש לא מומלץ לעלול לגרום למשתמש בעלול לגרום לשימוש לא מומלץ לעלול לגרום לשימוש לא מומלץ למענה מומלץ למחייבמשתמש בPeriodicTimerSystem.Threading.TimerDispatcherTimerCancellationToken(.NET)BackgroundServiceעיבוד מחזורי אסינכרונידרישה לביצוע מחזורי מדויקמאגר הת'רדים של ‎.NETחפיפה בין קריאות ה-callback של הטיימרcallback שמתנהג כ-async voidסיום התהליך עקב חריגה שלא נתפסה ב-ThreadPoolDispatcherPriorityחסימת ת'רד ממשק המשתמשאי-ביטול של מנוי לאירועSystem.Windows.Forms.Timerמנגנון מניעת כניסה חוזרת (Interlocked.Exchange וכדומה)Dispatcher(WPF)

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 18, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

1. קודם כול, המסקנה (במשפט אחד)

  • אם רוצים לכתוב עיבוד במרווח קבוע בטבעיות מבוססת await, קודם PeriodicTimer
  • אם רוצים להפעיל תקופתית callback קל על ה-ThreadPool,‏ System.Threading.Timer
  • אם רוצים לעדכן מסך בת’רד ה-UI של WPF,‏ DispatcherTimer
  • System.Threading.Timer — ה-callback עלול לחפוף. דחיפה רשלנית של עיבוד אסינכרוני פנימה נוטה להסתבך
  • DispatcherTimer מאפשר לגעת ב-UI ישירות, אבל אם מכניסים עיבוד כבד, קל שהוא יעצור גם את ה-UI
  • בהקשר של soft real-time מהמאמר הקודם, שלושת אלה אינם השחקנים הראשיים של המתנה מדויקת

בקיצור, מה שכדאי להסתכל עליו קודם הן שלוש הנקודות הבאות:

  1. באיזה ת’רד / הקשר רוצים להריץ
  2. האם רוצים לכתוב את גוף העיבוד ברצף עם async /‏ await
  3. האם אפשר לקבל חפיפה של ה-callback

רק ההפרדה בין השלושה הזו כבר מקטינה מאוד בלבול.

שלוש השאלות שבודקים תחילהתרשים המראה שההפרדה בין שלוש שאלות — באיזה ת'רד או הקשר רוצים להריץ, האם רוצים לכתוב ברצף עם async ו-await, והאם אפשר לקבל חפיפת callback — מקטינה מאוד בלבול בבחירת הטיימר.איפה רוצים להריץהבחירה בטיימר מתבררתרוצים לכתוב ברצף עם asyncאפשר לקבל חפיפת callback

איור 3: לפני שם הטיימר, מפרידים בין שלוש השאלות האלה.

2. קודם כול, לסדר בדף אחד

2.1. התמונה הכוללת

תרשים ההחלטה הכולל לבחירת טיימרתרשים המראה שאם רוצים להריץ בת'רד ה-UI בוחרים DispatcherTimer, אחרת אם רוצים לכתוב בפשטות עם async ו-await בוחרים PeriodicTimer, אחרת אם רוצים callback קל ב-ThreadPool בוחרים System.Threading.Timer, ואם לא משהו מאלה שוקלים תכנון אחר.כןלאכןלאכןלארוצים לעשות משהו במרווח קבוערוצים להריץ בת'רד ה-UI?DispatcherTimerרוצים לכתוב בפשטות עם async / await?PeriodicTimerרוצים callback קל ב-ThreadPool?System.Threading.Timerלשקול תכנון אחר — Channel / BackgroundService / event / waitable timer

איור 4: הסדר — ת’רד ה-UI, כתיבה עם async, ואז callback קל — קובע את שלושת הטיימרים.

בעבודה בפועל, ההסתעפות הזו מספיקה ברוב המקרים. כשמתלבטים, הכי בטוח לחתוך קודם: עיבוד אסינכרוני — PeriodicTimer, עדכון UI — DispatcherTimer.

System.Threading.Timer נוח, אבל יש לו הרגלים סביב חפיפת callback וניהול מחזור חיים, ולכן הוא קצת קפדני כבחירה ראשונה.

2.2. טבלת ההחלטה הראשונה

מצב הבחירה הראשונה היכן זה רץ הסיבה שזה מתאים נקודה ראשונה לתשומת לב
רוצים להריץ עיבוד async כמו HTTP /‏ DB / קובץ I/O במרווח קבוע PeriodicTimer בתוך זרימת שיטת ה-async הנוכחית אפשר לכתוב עם await, ועצירה וביטול פשוטים הנחת יסוד: טיימר אחד, consumer אחד. פיגור לא מקבל מקבילה אוטומטית
רוצים להריץ ב-ThreadPool heartbeat קל / שליחת מטריקות / בדיקת פקיעת cache System.Threading.Timer ThreadPool קל וקטן, מסוג callback. קל להעלות על תכנון מבוסס callback קיים ה-callback מניח כניסה חוזרת. עלולה להיות חפיפה. שומרים הפניה
רוצים תצוגת שעון ב-WPF או עדכון UI קל במרווח קבוע DispatcherTimer ה-Dispatcher (ת’רד ה-UI) של WPF אפשר לגעת ב-UI ישירות. יש עדיפות זמן ההפעלה המדויק לא מובטח. עיבוד כבד חוסם את ה-UI
הדיוק המחזורי הוא הנושא המרכזי, רוצים להימנע מהסתמכות על Sleep לא לשים את שלושת אלה במרכז - המטרה היא לא הרצה תקופתית של אפליקציה, אלא תכנון דיוק ההמתנה להסתכל בצד ה-event / waitable timer

החשוב בטבלה הזו הוא להסתכל על מקום הריצה ואופן הכתיבה, ולא על שם הטיימר. כשיש תקלה בבחירת טיימר, לרוב זה כי לא הסתכלו על “איפה זה רץ”, ולא בגלל שם ה-API.

נקודת המבט לבחירת טיימרתרשים המראה שבחירה לפי שם ה-API נוטה לתקלה כי לא מסתכלים איפה זה רץ, בעוד שבחירה לפי מקום הריצה ואופן הכתיבה נותנת החלטה שקשה לטעות בה.בחירה לפי שםתקלה כי לא בדקו איפה זה רץבחירה לפי מקום הריצה ואופן הכתיבהבחירה שקשה לטעות בה

איור 5: רוב התקלות נובעות מבחירה לפי שם. מה שצריך לבדוק הוא “איפה זה רץ” ו”איך רוצים לכתוב”.

3. מה כדאי להבחין בו קודם

3.1. סוג callback, או סוג שממתין ל-tick

אם מפרידים כאן, התמונה מתבהרת בבת אחת.

  • System.Threading.Timer ו-DispatcherTimer הם מסוג callback / event
  • PeriodicTimer הוא סוג שממתין ל-tick עם await

כלומר:

  • סוג callback: “צד הטיימר קורא”
  • PeriodicTimer: “אנחנו ממתינים ל-tick הבא”

זה ההבדל.

אם גוף העיבוד הוא async, ורוצים לקרוא את “להמתין ← לעבד ← להמתין שוב” כזרימה אחת רציפה, PeriodicTimer טבעי יותר.

מנגד:

  • רוצים להעלות על תכנון קיים מבוסס callback
  • גוף העיבוד קצר וסינכרוני
  • פשוט רוצים בעיטה תקופתית

במצבים כאלה, System.Threading.Timer מתאים.

PeriodicTimer נוח, אבל הוא לא פותר הכול. זה לא מניח שליחת כמה WaitForNextTickAsync בו-זמנית לאותו טיימר, וגם אם מתרחשים כמה tick בזמן שלא ממתינים, הם מתקפלים לתוך אחד.

חשוב לא לטעות ולחשוב “זה ישלים פיגור מעצמו”.

סוג callback לעומת סוג שממתין ל-tickתרשים המראה ש-System.Threading.Timer ו-DispatcherTimer הם מסוג callback שצד הטיימר קורא, ו-PeriodicTimer הוא סוג שאנחנו ממתינים ל-tick הבא עם await.סוג callbackסוג שממתין ל-tickאיזה סוג?צד הטיימר קוראאנחנו ממתינים ל-tick הבאTimer ו-DispatcherTimerPeriodicTimerלהמתין ← לעבד ← להמתין שוב, ברצף אחד

איור 6: “נקרא” או “ממתין” — ההבחנה הזו לבדה כבר מבהירה הרבה.

3.2. רץ ב-ThreadPool, או בת’רד ה-UI

הדבר הבא שכדאי לבדוק הוא איפה זה מתבצע.

ה-callback של System.Threading.Timer רץ ב-ThreadPool, לא בת’רד שיצר אותו. לכן הוא מתאים לעיבוד ברקע, אבל לא בהנחה שנוגעים ישירות ב-UI.

מצד שני, DispatcherTimer הוא טיימר UI שמשולב בתור ה-Dispatcher. ב-WPF, הוא רץ על אותו Dispatcher, כך שאפשר לעדכן UI ישירות בתוך מטפל ה-Tick.

ההבדל הזה משמעותי מאוד.

  • כדי לגעת ב-UI מטיימר ThreadPool, צריך להחזיר במפורש ל-UI
  • ‏DispatcherTimer מקל לגעת ב-UI, אבל בתמורה צורך זמן מת’רד ה-UI

כלומר, היתרון של DispatcherTimer הוא “אפשר לגעת ב-UI בבטחה”, אבל זה גם אומר “אם מכניסים עיבוד כבד, זה גורר גם קלט וציור מחדש”.

ההבדל במקום הריצה והמחיר שלותרשים המראה שה-callback של System.Threading.Timer רץ ב-ThreadPool ודורש החזרה מפורשת כדי לגעת ב-UI, בעוד DispatcherTimer רץ בת'רד ה-UI ומאפשר לגעת ב-UI ישירות, אך זה עולה בצריכת זמן שמכבידה על עיבוד כבד.Timer: רץ ב-ThreadPoolלגעת ב-UI דורש החזרה מפורשתDispatcherTimer: ת'רד ה-UIאפשר לגעת ב-UI ישירותעיבוד כבד גורר קלט וציור

איור 7: ההבדל במקום הריצה הוא בעצם עסקת חליפין בין קלות הנגיעה ב-UI לצריכת זמן ת’רד ה-UI.

3.3. עיבוד מחזורי והבטחת דיוק הם נושאים שונים

הנקודה הזו חשובה כהמשך למאמר הקודם.

גם אם הניסוח “לעשות משהו במרווח קבוע” זהה,

  • מבחינת האפליקציה, רוצים עיבוד תקופתי כל כמה שניות
  • ברמה של מילישנייה עד כמה מילישניות, רוצים להתקרב ל-deadline ככל האפשר

אלה שני נושאים שונים.

System.Threading.Timer הוא טיימר קל וקל לשימוש, אבל לא כלי ייעודי לדיוק. גם DispatcherTimer מושפע מנסיבות תור ה-Dispatcher ומהעדיפות.

גם PeriodicTimer, אם מסתכלים רק על השם, נראה “כאילו המחזור מדויק”, אבל היתרון שלו בעבודה בפועל הוא לא דיוק (precision), אלא קלות כתיבת זרימת async.

לכן:

  • רוצים לכתוב הרצה תקופתית של האפליקציה
  • או לכוונן דיוק המתנה

בטוח יותר להפריד ביניהם כבר בהתחלה.

אם שני הנושאים האלה מתערבבים, הדיון בבחירת הטיימר נוטה לגלוש לכיוון מוזר.

הפרדה בין הרצה תקופתית לדיוק המתנהתרשים המראה שרצון האפליקציה לעיבוד תקופתי כל כמה שניות שונה מרצון להתקרב ל-deadline ברמת מילישנייה, ששלושת הטיימרים לא מיועדים לדיוק.רוצים לעשות משהו במרווח קבועהרצה תקופתית של האפליקציהרוצים לכוונן דיוק המתנהכאן נכנסים שלושת הטיימריםעוברים לתכנון אופן ההמתנה עצמושלושת הטיימרים אינם כלי דיוק ייעודי

איור 8: גם אם הניסוח “מרווח קבוע” זהה, הרצה תקופתית והבטחת דיוק הם נושאים שונים.

4. דפוסים אופייניים

4.1. לעיבוד תקופתי אסינכרוני — PeriodicTimer

ב-worker,‏ BackgroundService, או עיבוד שרץ ברקע בקונסולה, אם רוצים להריץ עיבוד async במרווח קבוע, PeriodicTimer הכי קל לכתיבה בתור בחירה ראשונה.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public sealed class CacheRefreshWorker : BackgroundService
{
    private readonly ILogger<CacheRefreshWorker> _logger;

    public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("CacheRefreshWorker started.");

        await RefreshCacheAsync(stoppingToken);

        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                await RefreshCacheAsync(stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("CacheRefreshWorker stopping.");
        }
    }

    private async Task RefreshCacheAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Refreshing cache...");
        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
    }
}

היתרון של הצורה הזו:

  • קל לעקוב אחרי זרימת הקוד כשיטת async אחת
  • קל להעביר את CancellationToken ישירות למורד הזרם
  • אפשר לצמצם ניהול מחזור חיים וחריגות מבוסס callback

בפרט, כשגוף העיבוד:

  • קורא ל-HTTP
  • שואל DB
  • קורא קובץ
  • עושה await ל-API אסינכרוני אחר

כלומר מבוסס בעיקר על המתנת I/O — ההתאמה טובה מאוד.

שתי נקודות לתשומת לב:

  1. משתמשים בהנחת יסוד של טיימר אחד ל-consumer אחד
  2. קובעים בעצמכם מדיניות למקרה שהעיבוד ארוך מהמחזור

PeriodicTimer לא רץ במקביל באופן אוטומטי כדי להשלים פיגור, גם אם העיבוד הקודם התארך. במובן הזה, זה טיימר ל”כתיבה טבעית של לולאת async במרווח קבוע”.

אם חושבים גם על נוחות בדיקה, נוח בשקט שאפשר להשתמש בבנאי (constructor) שמקבל TimeProvider.

זרימת ההרצה התקופתית עם PeriodicTimerתרשים המראה שממתינים עם WaitForNextTickAsync, מריצים את גוף העיבוד ה-async, וממתינים שוב, וכשה-token מתבטל יוצאים מהלולאה, כשעיבוד שמתעכב לא רץ במקביל באופן אוטומטי.ביטול ה-tokenהמתנה עם WaitForNextTickAsyncהרצת גוף העיבוד ה-asyncיציאה מהלולאה ועצירהפיגור לא מקבל מקבילה אוטומטית

איור 9: PeriodicTimer מאפשר לכתוב “להמתין ← לעבד ← להמתין שוב” כזרימה אחת של שיטת async.

4.2. להרצת callback קל ב-ThreadPool — System.Threading.Timer

אם רק רוצים לקרוא תקופתית ל-callback קצר, System.Threading.Timer פשוט.

לדוגמה:

  • שליחת heartbeat
  • איסוף מטריקות קלות
  • בדיקת פקיעת תוקף קצרה
  • תלייה על תכנון קיים מבוסס callback

מצבים כאלה.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public sealed class HeartbeatService : IHostedService, IDisposable
{
    private readonly ILogger<HeartbeatService> _logger;
    private Timer? _timer;
    private int _running;

    public HeartbeatService(ILogger<HeartbeatService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }

    private void OnTimer(object? state)
    {
        if (Interlocked.Exchange(ref _running, 1) != 0)
        {
            return;
        }

        try
        {
            _logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
        }
        finally
        {
            Volatile.Write(ref _running, 0);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

בדוגמה הזו הוכנס Interlocked.Exchange כי System.Threading.Timer לא ממתין לסיום ה-callback הקודם.

זו נקודה חשובה מאוד.

  • ה-callback רץ ב-ThreadPool
  • ה-callback מניח כניסה חוזרת (reentrant)
  • אם העיבוד ארוך מהמרווח, ייתכן חפיפה

אפשר לראות את “החפיפה האפשרית” הזו בפועל בבדיקת היחידה בדוגמה. אם מכניסים לטיימר במחזור של 50ms עיבוד שאורך 300ms, וסופרים את מספר ההרצות המקבילות המרבי, מקבלים 2 ומעלה. בגרסה עם אותה שמירה עם Interlocked.Exchange כמו למעלה, גם באותם תנאים ההרצה המקבילית המרבית נשארת 1, ובמקום זאת ה-callback שהופעל בזמן ריצה מדולג.

// קטע מתוך tests/KomuraSoft.TimerSelection.Tests/ThreadPoolTimerOverlapTests.cs

// בלי שמירה: מחזור 50ms מול עיבוד 300ms. ממתינים עד שנצפית חפיפה
bool overlapped = await WaitUntilAsync(
    () => Volatile.Read(ref maxObserved) >= 2,
    TimeSpan.FromSeconds(10));

Assert.True(overlapped, "timer callbacks did not overlap within the timeout.");

// עם שמירה: גם באותם תנאים, ההרצה המקבילית המרבית נשארת 1
Assert.Equal(1, Volatile.Read(ref maxConcurrent));

אם העיבוד לא קל, אפשר:

  • לדלג על הפעלה כפולה
  • להכניס לתור
  • לעבור ל-PeriodicTimer

לתכנן כך רגוע יותר.

חפיפת callback ושמירה מפניהתרשים המראה שכיוון ש-System.Threading.Timer לא ממתין לסיום ה-callback הקודם, אם העיבוד ארוך מהמרווח יש חפיפה. שמירה עם Interlocked.Exchange משאירה הרצה מקבילית אחת ומדלגת על ההפעלה הנוכחית.בלי שמירהעם שמירההפעלת callback בכל מחזורהקודם עדיין רץ?ה-callback רץ בחפיפהדילוג על ההפעלה הנוכחיתההרצה המקבילית נשארת 1

איור 10: בטיימר שלא ממתין לסיום הקודם, מחליטים בעצמכם אם לאפשר חפיפה או לחסום עם שמירה.

עוד נקודה חשובה בשקט היא להחזיק בהפניה. ‏System.Threading.Timer נהיה מועמד ל-GC אם ההפניה נעלמת, גם אם הוא עדיין פעיל. גם מיד אחרי קריאה ל-Dispose(), ייתכן שה-callback שכבר הוכנס לתור ירוץ אחר כך.

כלומר, System.Threading.Timer הוא:

  • קל משקל
  • מהיר
  • פשוט

אבל בתמורה, זהו טיימר שבו אנחנו אחראים על נסיבות ה-callback.

נקודות לתשומת לב במחזור חיי System.Threading.Timerתרשים המראה שיש להחזיק בהפניה ל-System.Threading.Timer, אחרת הוא נהיה מועמד ל-GC, ושגם אחרי Dispose ייתכן ש-callback שכבר בתור ירוץ מאוחר יותר.System.Threading.Timerהמשך החזקת הפניהאם ההפניה נעלמת, מועמד ל-GCסידור עם Disposecallback שכבר בתור עלול לרוץ אחר כך

איור 11: קל משקל, אבל בתמורה גם החזקת ההפניה וגם ה-callback שאחרי Dispose באחריותנו.

4.3. לעדכון UI ב-WPF — DispatcherTimer

ב-WPF, אם רוצים לעדכן תקופתית שעון על המסך או תצוגת מצב קלה, DispatcherTimer טבעי.

using System;
using System.Windows;
using System.Windows.Threading;

public partial class MainWindow : Window
{
    private readonly DispatcherTimer _clockTimer;

    public MainWindow()
    {
        InitializeComponent();

        _clockTimer = new DispatcherTimer(DispatcherPriority.Background)
        {
            Interval = TimeSpan.FromSeconds(1)
        };
        _clockTimer.Tick += ClockTimer_Tick;
        _clockTimer.Start();
    }

    private void ClockTimer_Tick(object? sender, EventArgs e)
    {
        ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
    }

    protected override void OnClosed(EventArgs e)
    {
        _clockTimer.Stop();
        _clockTimer.Tick -= ClockTimer_Tick;
        base.OnClosed(e);
    }
}

DispatcherPriority.Background שמועבר לבנאי מציין באיזו עדיפות בתור ה-Dispatcher מעובד ה-Tick. גם new DispatcherTimer() בלי ארגומנטים מגיע כברירת מחדל ל-Background, כך שכאן זו רק הצהרה מפורשת של ברירת המחדל — ההתנהגות לא משתנה. Background (ערך 4) היא עדיפות של “מעבדים אחרי שכל שאר העיבוד הלא-idle הסתיים”, נמוכה מ-Input (5) ומ-Render (7). כלומר, זה לא דוחף את הטיפול בקלט או בציור כדי להריץ Tick. זה מתאים לשימושים כמו תצוגת שעון, שבהם “קצת סטייה זה בסדר, אבל לא רוצים להפריע לפעולה”. אם רוצים להוציא את תוצאת ה-Tick למסך מהר ככל האפשר, יש גם אפשרות להעלות עד Normal (9), אבל אז ההנחה שגוף ה-Tick נשאר קל מתחזקת.

המיקום של DispatcherPriorityתרשים המראה ש-Background מעבד אחרי שכל שאר העיבוד הלא-idle הסתיים, נמוך מ-Input ו-Render, ולא דוחק קלט וציור מהתור, ושאם רוצים מהירות אפשר להעלות עד Normal בתמורה לשמירה על Tick קל.ה-Tick של DispatcherTimerנכנס בעדיפות Background(ערך 4)מעובד אחרי קלט וציורמתאים לשעון שלא מפריע לפעולהאם דוחק, לעלות ל-Normal(ערך 9)בתמורה, Tick חייב להישאר קל

איור 12: עדיפות ה-Background כברירת מחדל ממקמת את ה-Tick במקום שלא דוחק קלט וציור.

היתרון של DispatcherTimer הוא שה-Tick מעובד על ה-Dispatcher של WPF, ולכן אפשר לגעת ב-UI ישירות.

לדוגמה, זה מתאים ל:

  • תצוגת שעון
  • עדכון תצוגה קל של מצב חיבור
  • טריגר להערכה מחדש של Command
  • עדכון קל של מספרים שמוצגים על המסך

מצבים כאלה.

עם זאת, גם כאן יש נקודה שמשנה את האווירה.

DispatcherTimer רץ בת’רד ה-UI, ולכן אם מכניסים עיבוד כבד למטפל ה-Tick, זה גורר איתו את הקלט, הציור ואף את מיקום מחדש ומאט אותם.

בנוסף, DispatcherTimer הוא לא כלי שמבטיח “בדיוק בזמן שנקבע”. הוא מושפע מעבודות אחרות בתור ה-Dispatcher ומהעדיפות.

לכן, בעבודה בפועל:

  • לשמור על תוכן ה-Tick קל
  • להעביר I/O או CPU כבדים למקום אחר
  • בסגירה, לציין במפורש את מחזור החיים עם Stop() וביטול המנוי

מודעות לזה יוצרת יציבות.

שלוש נקודות ליציבות DispatcherTimerתרשים המראה שיציבות מושגת אם תוכן ה-Tick נשאר קל, עיבוד כבד של I/O או CPU מועבר לרקע, ומחזור החיים מצוין במפורש עם Stop וביטול מנוי בסגירה.תפעול DispatcherTimerTick נשאר קלעבודה כבדה עוברת לרקעStop וביטול מנוי מציינים מחזור חייםכי זה צורך זמן מת'רד ה-UI

איור 13: בתמורה לנוחות הנגיעה הישירה ב-UI, נדרש תפעול שמשאיר את ה-Tick קל ומציין מחזור חיים.

4.4. לעיבוד מחזורי בכיוון soft real-time — כלי אחר

זו נקודת החיבור למאמר הקודם.

מה שנדון במאמר הקודם על soft real-time לא היה “בערך לרוץ כל כמה שניות”, אלא איך מצמצמים תנודתיות במחזור ופספוסי deadline.

בהקשר הזה:

  • לא להסתמך על המתנה יחסית עם Sleep
  • להשתמש בגישה מונחית-אירועים או ב-waitable timer
  • להפריד בין fast path ל-slow path
  • למדוד פיגור

אלה הנושאים המרכזיים.

לכן:

  • עיבוד תקופתי async רגיל של אפליקציה ← PeriodicTimer
  • callback על ThreadPool ← System.Threading.Timer
  • עדכון UI ← DispatcherTimer
  • דיוק מחזורי כשלעצמו ← עולם המאמר הקודם

נקי יותר להפריד את הבעיה כבר מההתחלה כך.

השאלה “רוצה לרוץ בדיוק ככל האפשר כל 1ms. איזה טיימר .NET מתאים?” היא בערך בחצי כבר לא שאלה של בחירת טיימר, אלא בעיה של אופן ההמתנה והתכנון.

חלוקת הבעיה לארבעה מראשתרשים המראה שעיבוד תקופתי async רגיל שייך ל-PeriodicTimer, callback על ThreadPool שייך ל-System.Threading.Timer, עדכון UI שייך ל-DispatcherTimer, ודיוק מחזורי כשלעצמו שייך לתכנון אופן ההמתנה מהמאמר על soft real-time.מה רוצים לעשותasync: PeriodicTimercallback: TimerUI: DispatcherTimerדיוק: תכנון אופן ההמתנה

איור 14: לפני “איזה טיימר”, נקי יותר לחלק כבר מההתחלה לאיזו בעיה זו שייכת.

5. אנטי-דפוסים נפוצים

5.1. העברת lambda של async ישירות ל-System.Threading.Timer

זה קורה הרבה.

_timer = new Timer(async _ => await RefreshAsync(), null,
    TimeSpan.Zero, TimeSpan.FromSeconds(5));

זה נראה נקי, אבל TimerCallback הוא מסוג void. כלומר, ה-lambda הזו של async הופכת בפועל להתנהגות מסוג async void.

כתוצאה מכך:

  • הצד הקורא לא יכול לעשות await
  • אי אפשר להמתין לסיום
  • ניהול חריגות קשה
  • צריך גם לחשוב בנפרד על חפיפת ה-callback

מצב לא נוח.

שווה לפרט קצת יותר את הסיבה לכך שניהול החריגות קשה. ב-async Task, החריגה נטענת על ה-Task, כך שהקורא מקבל אותה כשעושה await. ל-async void (או המקביל לו) אין Task כזה, ולכן החריגה שנזרקת נזרקת מחדש ישירות אל ה-SynchronizationContext שהיה בתוקף כשהשיטה התחילה. ה-callback של System.Threading.Timer רץ על ה-ThreadPool, ושם אין SynchronizationContext. כתוצאה מכך, החריגה הופכת לחריגה בלתי מטופלת בת’רד ה-ThreadPool, וכברירת מחדל היא מפילה את כל התהליך. אלא אם עוטפים את פנים ה-callback ב-try /‏ catch בעצמנו, אי אפשר לתפוס אותה בחוץ.

לאן הולכת חריגה מ-lambda async שהועברה ל-callbackתרשים המראה ש-TimerCallback הוא void, ולכן lambda של async שהועברה אליו הופכת ל-async void, והחריגה נזרקת מחדש להקשר ה-SynchronizationContext בזמן ההתחלה, אבל ב-ThreadPool אין הקשר כזה, ולכן היא הופכת לחריגה בלתי מטופלת שמפילה את התהליך כברירת מחדל.אין ב-ThreadPoolחריגה בתוך lambda של asyncהתנהגות async void ביציאהיש SynchronizationContext?הופכת לחריגה בלתי מטופלת ב-ThreadPoolכברירת מחדל, מפילה את כל התהליךרק try / catch עצמי מונע את זה

איור 15: החריגה מה-lambda הנקייה למראה של async, ללא מקום לתפוס אותה, מפילה את התהליך.

אם גוף העיבוד הוא async, קריא יותר לשקול קודם PeriodicTimer.

5.2. הכנסת עיבוד כבד ל-Tick של DispatcherTimer

מכיוון ש-DispatcherTimer מאפשר לגעת ב-UI ישירות, מתפתים לכתוב שם הכול. אבל זה ת’רד ה-UI.

  • עיבוד סינכרוני ארוך
  • חישוב CPU כבד
  • I/O חוסם
  • עיבוד עם await ארוך שעלול להתחיל פעמיים

אם מכניסים את אלה, זה מתנגש חזיתית עם קלט וציור ה-UI.

יציב יותר לשמור על Tick קל, להעביר עבודה כבדה לרקע, ולהחזיר ל-UI רק את התוצאה הנדרשת.

5.3. לחשוב ש-PeriodicTimer משלים פיגור באופן אוטומטי

גם כאן קל לטעות.

PeriodicTimer מצוין ככלי לכתיבה נקייה של לולאת async במרווח קבוע, אבל אם העיבוד הקודם התארך, הוא לא רץ במקביל באופן אוטומטי כדי להשלים את הפיגור.

אפשר לוודא את זה גם בבדיקת היחידה בדוגמה. אם משאירים טיימר במחזור של 250ms במצב שאף אחד לא ממתין למשך 1.5 שניות ואז ממתינים, ההמתנה הראשונה מתמלאת מיד בזכות מה שהצטבר, אבל ההמתנה השנייה לא מתמלאת מיד. כלומר, כמה tick שהצטברו בזמן ההזנחה מתקפלים לתוך אחד בלבד.

// קטע מתוך tests/KomuraSoft.TimerSelection.Tests/PeriodicTimerBehaviorTests.cs

using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(250));
await Task.Delay(TimeSpan.FromMilliseconds(1500)); // בזמן הזה, אף אחד לא ממתין

// ההמתנה הראשונה מתמלאת מיד בזכות ה-tick שהצטבר
ValueTask<bool> first = timer.WaitForNextTickAsync();
Assert.True(first.IsCompleted);
Assert.True(await first);

// ההמתנה השנייה לא מתמלאת מיד (לא נשארו כמה tick)
ValueTask<bool> second = timer.WaitForNextTickAsync();
Assert.False(second.IsCompleted);

מכיוון שה-tick שמתרחשים בזמן שלא ממתינים עלולים להתקפל לתוך אחד,

  • האם לדלג בפיגור
  • האם מספיק להסתכל רק על העדכני ביותר
  • האם רוצים בהכרח לעבד את כל הכמות

אלה צריכים להיקבע בתכנון.

קיפול ה-tick בזמן שלא ממתיניםתרשים המראה שאם PeriodicTimer מקבל כמה tick בזמן שאף אחד לא ממתין, הם מתקפלים לתוך אחד, ואז צריך להחליט בתכנון האם לדלג בפיגור, להסתכל רק על העדכני, או לעבד את כל הכמות.כמה tick בזמן שאף אחד לא ממתיןמתקפלים לתוך אחדאיך מתייחסים לפיגור?דילוגהסתכלות רק על העדכניעיבוד כל הכמות

איור 16: ה-tick שהצטברו מתקפלים לתוך אחד. אופן השלמת הפיגור נקבע בתכנון, לא אוטומטית.

5.4. דחיית ניהול העצירה ומחזור החיים

בטיימרים, יש יותר תקלות בעצירה מאשר בהפעלה.

מה שקל לפספס הוא בערך אלה:

  • יוצרים System.Threading.Timer כמשתנה מקומי בלי להחזיק בהפניה
  • לא עוצרים את System.Threading.Timer, וסביב Dispose() נשאר מעורפל
  • לא עושים Stop() ל-DispatcherTimer, וגם לא מבטלים את מנוי ה-Tick
  • גם אחרי סגירת המסך, הטיימר ממשיך למשוך את מחזור חיי האובייקט

בפרט DispatcherTimer עלול להמשיך להחזיק חי את האובייקט שהשיטה שלו קשורה (bind) אליו. אם מתקבלת תחושה מוזרה של “ה-Window הזה אמור היה להיסגר, אבל הוא עדיין נשאר” — כדאי לחשוד כאן.

מלכודות בעצירה ובניהול מחזור חייםתרשים המראה ש-Timer בלי החזקת הפניה או עם Dispose מעורפל משאיר את דרך העצירה לא ברורה, ו-DispatcherTimer בלי Stop וביטול מנוי ממשיך להחזיק חי את האובייקט הקשור אליו, מה שגורם ל-Window שאמור היה להיסגר להישאר.Timer בלי החזקת הפניהדרך העצירה נשארת מעורפלתDispose מעורפלבלי Stop ובלי ביטול מנויממשיך להחזיק חי את הקשורWindow שהיה אמור להיסגר נשאר

איור 17: יש יותר תקלות בעצירה מאשר בהפעלה. כותבים את הסידור בסיום כבר מההתחלה.

6. רשימת בדיקה לסקירת קוד

  • אפשר להסביר אם העיבוד המחזורי הזה נכתב כעדכון UI /‏ callback על ThreadPool / לולאת async
  • האם גוף העיבוד הוא async, אבל בכל זאת נדחף בכוח לתוך טיימר מסוג callback
  • אם משתמשים ב-System.Threading.Timer, האם אפשר לעמוד בחפיפת callback, או שיש שמירה מפניה
  • האם ל-Tick של DispatcherTimer לא נכנס עיבוד כבד, I/O חוסם או עיבוד סינכרוני ארוך
  • אם משתמשים ב-PeriodicTimer, האם נקבעה מדיניות למקרה של פיגור
  • האם שיטת העצירה (Change /‏ Dispose /‏ Stop) והזרימה בסיום האפליקציה ברורות
  • האם ההפניה ל-System.Threading.Timer נשמרת כראוי
  • האם יש ביטול מנוי וסידור בסגירת מסך ל-DispatcherTimer
  • האם הופרד מראש אם הבעיה היא “הרצה תקופתית של האפליקציה” או “דיוק המתנה”

7. חלוקה גסה בין המצבים

נשים כאן קני מידה לעבודה בפועל.

  • רוצים לקרוא ל-API כל 30 שניות ולעדכן הגדרות ← PeriodicTimer
  • רוצים לשלוח heartbeat או מטריקות קלות כל 5 שניות ← System.Threading.Timer
  • רוצים תצוגת שעון או עדכון סטטוס קל ב-WPF ← DispatcherTimer
  • רוצים לגעת ב-UI ישירות בכל Tick ← DispatcherTimer
  • גוף העיבוד התקופתי מלא ב-await, ורוצים לטפל בטבעיות גם בעצירה וגם בחריגות ← PeriodicTimer
  • רוצים בעיטה קטנה מבוססת callback בעלות נמוכה ← System.Threading.Timer
  • דיוק מחזורי ברמת 1–5ms הוא הליבה ← לפני שלושת אלה, להסתכל על אופן ההמתנה מהמאמר הקודם

בגסות רבה, בשורה אחת:

  • PeriodicTimer הוא טיימר בשביל async
  • System.Threading.Timer הוא טיימר בשביל callback על ThreadPool
  • DispatcherTimer הוא טיימר בשביל UI

עם דרך הזכירה הזו, קשה לטעות בגדול.

8. סיכום

מה שבאמת חשוב בבחירת טיימר ב-‏.NET הוא לא ההבדל בשמות, אלא שלוש הנקודות האלה:

  1. איפה זה רץ
  2. באיזו זרימה רוצים לכתוב
  3. איך מטפלים בחפיפה ובפיגור

בתור מדיניות, זה מספיק כדי להתמודד היטב:

  1. עיבוד תקופתי async ← PeriodicTimer
  2. callback קל על ThreadPool ← System.Threading.Timer
  3. עדכון UI ב-WPF ← DispatcherTimer
  4. דיוק הוא הליבה ← אופן המתנה אחר

הטיימרים מתערבבים כי השמות שלהם דומים. אבל התפקידים לא ממש דומים.

  • PeriodicTimer הוא כלי לסידור זרימת async
  • System.Threading.Timer הוא כלי לבעיטה תקופתית של callback
  • DispatcherTimer הוא כלי לעדכון תקופתי בת’רד ה-UI

רק חשיבה נפרדת על שלושת אלה כבר מרגיעה מאוד את הקוד.

סיכום תפקידי שלושת הטיימריםתרשים המראה ש-PeriodicTimer הוא כלי לסידור זרימת async, System.Threading.Timer הוא כלי לבעיטה תקופתית של callback, DispatcherTimer הוא כלי לעדכון תקופתי בת'רד ה-UI, ולדיוק פונים לתכנון אופן ההמתנה.תפקידי הטיימריםPeriodicTimer: asyncTimer: callbackDispatcherTimer: UIלדיוק, לתכנון אופן ההמתנה

איור 18: השמות דומים, אבל התפקידים לא. עם החלוקה לשלוש הזו, קשה לטעות בגדול.

מנגד, אם זה מתערבב:

  • מה שאמור להיות async נהיה דומה ל-async void
  • נגיעה ישירה ב-UI וקריסה
  • חפיפת callback ומצב מעורפל
  • גם דיון הדיוק המחזורי מתערבב

וקורים דברים מטרידים למדי בשגרה.

קודם כול, מסתכלים מ”איפה רוצים להריץ את זה”. רק זה כבר הופך את בחירת הטיימר להרבה יותר רגועה.

9. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה ההבדל בין PeriodicTimer ל-System.Threading.Timer?
ההבדל הגדול ביותר הוא ש-PeriodicTimer הוא טיפוס שממתין ל-tick עם await, ואילו System.Threading.Timer הוא טיפוס מסוג callback. את PeriodicTimer אפשר לכתוב כ"להמתין ← לעבד ← להמתין שוב" בתוך שיטת async אחת ברצף, וקל להעביר את ה-CancellationToken גם למורד הזרם. לעומת זאת, ה-callback של System.Threading.Timer רץ ב-ThreadPool, ולא ממתין לסיום ה-callback הקודם, כך שאם העיבוד ארוך יותר מהמרווח, ייתכן חפיפה. לעיבוד תקופתי אסינכרוני מתאים PeriodicTimer, ולהפעלה תקופתית קלה וסינכרונית של callback מתאים System.Threading.Timer.
האם PeriodicTimer משלים באופן אוטומטי פיגור בעיבוד?
לא. גם אם העיבוד הקודם התארך, זה לא רץ במקביל באופן אוטומטי כדי להשלים את הפיגור. גם אם מתרחשים כמה tick בזמן שלא ממתינים, הם מתקפלים לתוך אחד בלבד. לכן, האם לדלג בפיגור, להסתכל רק על העדכני ביותר, או לעבד את כל הכמות — זו החלטת תכנון. גם לא מדובר בהנחת יסוד של שליחת כמה WaitForNextTickAsync בו-זמנית לאותו טיימר.
אסור להעביר למדה (lambda) של async ל-System.Threading.Timer?
עדיף להימנע מזה. TimerCallback הוא מסוג void, ולכן ה-lambda של async שמעבירים הופכת בפועל להתנהגות מסוג async void. כתוצאה מכך, הצד הקורא לא יכול לעשות await, אי אפשר להמתין לסיום, ניהול החריגות נהיה קשה, וגם צריך לחשוב בנפרד על חפיפת ה-callback. אם גוף העיבוד הוא async, קריא ובטוח יותר לשקול קודם PeriodicTimer.
מתי כדאי להשתמש ב-DispatcherTimer?
משתמשים בו כשרוצים לעדכן UI באופן תקופתי ב-WPF, כמו תצוגת שעון או תצוגת סטטוס קלה. מכיוון ש-Tick מעובד על ה-Dispatcher (ת'רד ה-UI) של WPF, היתרון הוא שאפשר לגעת ב-UI ישירות בתוך המטפל. עם זאת, מכיוון שזה רץ בת'רד ה-UI, אם מכניסים ל-Tick עיבוד כבד או I/O חוסם, זה גורר איתו גם את הקלט והציור ומאט אותם. גם אין הבטחה להפעלה בדיוק בזמן שנקבע. יציב יותר לשמור על Tick קל, להעביר עבודה כבדה לרקע, ולציין במפורש את מחזור החיים עם Stop() וביטול המנוי כשסוגרים את המסך.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג