היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 12 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173401)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). שלושה timers ב-.NET — PeriodicTimer, Timer ו-DispatcherTimer. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173401 https://comcomponent.com/he/blog/periodictimer-system-threading-timer-dispatchertimer-guide/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173401
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173402
במאמר הקודם, מדריך מעשי למקסום soft real-time ב-Windows רגיל — checklist ראשונה, כיסינו את הרעיון של הימנעות מלולאה מחזורית שמסתמכת על Sleep, ושימוש בגישה event-driven או ב-waitable timer. במשפט אחד, המסקנה של המאמר הקודם הייתה: אם רוצים לצמצם jitter במחזור ופספוסי deadline, קודם מתכננים את עצם “אופן ההמתנה”, לפני סוג ה-timer.
אז מה עושים בפיתוח אפליקציות .NET רגיל יותר?
כאן קל להתבלבל בין PeriodicTimer, System.Threading.Timer ו-DispatcherTimer.
כולם נקראים “timer”, אבל:
- timer שממתין ל-tick עם
await - timer שה-callback שלו מגיע מ-ThreadPool
- timer שרץ על ה-
Dispatcherשל ה-UI thread
כלומר, האופי שלהם שונה מאוד.
flowchart TB
accTitle: שלושה timers עם שמות דומים אך אופי שונה
accDescr: תרשים המראה ש-PeriodicTimer הוא timer שממתין ל-tick עם await, System.Threading.Timer הוא timer שה-callback שלו מגיע מ-ThreadPool, ו-DispatcherTimer הוא timer שרץ ב-UI thread.
t["שלושה timers"] --> pt["PeriodicTimer: ממתין עם await"]
t --> st["Timer: ה-callback מגיע"]
t --> dt["DispatcherTimer: UI thread"]
איור 1: כולם נקראים timer, אבל אופן ההמתנה ומקום הריצה שונים לגמרי.
בעבודה בפועל, מה שקל להתבלבל בו הוא בערך אלה:
- מעבירים lambda של async ל-System.Threading.Timer, למרות שזה עיבוד תקופתי אסינכרוני
- נוגעים ישירות במסך מ-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 בתור timer ה-UI.
המאמר הזה מטה את ההסבר בצד ה-UI ל-WPF, אבל גם קוראי WinForms נכללים בקהל היעד. אם קוראים DispatcherTimer כ-System.Windows.Forms.Timer, ההערות בסעיפים 4.3 ו-5.2 חלות כמו שהן. עם זאת, יש שני הבדלים:
System.Windows.Forms.Timerהוא timer חד-thread שה-Tick שלו קורה דרך ה-message loop, ואין לו ציון עדיפות (DispatcherPriority) כמו ל-DispatcherTimer- בתיעוד של Microsoft נקבע שהדיוק מוגבל לכ-55 מילישניות. הוא לא מתאים למחזורים דקים — במקרה כזה כדאי לשקול timer שאינו timer UI
יש לציין: הנושא כאן הוא איך כותבים הרצה תקופתית בצד האפליקציה. כשהדיוק המחזורי עצמו הוא הנושא, חוזרים לדיון במאמר הקודם על soft real-time.
flowchart TB
accTitle: תחום הכיסוי של המאמר הזה
accDescr: תרשים המראה שאיך כותבים הרצה תקופתית בצד האפליקציה הוא תחום המאמר הזה, ואילו כשהדיוק המחזורי עצמו הוא הנושא חוזרים למאמר הקודם על soft real-time שעסק בתכנון אופן ההמתנה.
q{"מה הנושא?"}
q -->|"איך כותבים הרצה תקופתית באפליקציה"| here["דיון שלושת ה-timers במאמר הזה"]
q -->|"הדיוק המחזורי עצמו"| rt["הדיון הקודם על תכנון אופן ההמתנה"]
איור 2: גם אם זה “לעשות משהו במרווח קבוע” — אופן כתיבת ההרצה התקופתית ותכנון דיוק המחזור הם בעיות שונות.
כמו כן, הקוד שמופיע במאמר הזה פורסם ב-GitHub כחבילת דוגמאות מלאה שאפשר לבנות ולהריץ (ספרייה ודמו קונסולה של PeriodicTimer / System.Threading.Timer, ובדיקות יחידה שבודקות את קיפול ה-tick ואת overlap של ה-callback).
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
תוכן עניינים
- קודם כל, המסקנות
- מבט אחד על התמונה
- 2.1. סקירה
- 2.2. טבלת החלטה
- מה כדאי להבחין בו קודם
- 3.1. סוג callback, או סוג שממתין ל-tick
- 3.2. רץ ב-ThreadPool, או ב-UI thread
- 3.3. עיבוד מחזורי והבטחת דיוק הם נושאים שונים
- דפוסים נפוצים
- 4.1. לעיבוד תקופתי אסינכרוני —
PeriodicTimer - 4.2. להרצת callback קל ב-ThreadPool —
System.Threading.Timer - 4.3. לעדכון UI ב-WPF —
DispatcherTimer - 4.4. לעיבוד מחזורי בכיוון soft real-time — כלי אחר
- 4.1. לעיבוד תקופתי אסינכרוני —
- anti-patterns נפוצים
- checklist ל-code review
- איך בוחרים בפועל
- סיכום
- מקורות
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 18, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
1. קודם כל, המסקנות
- אם רוצים לכתוב עיבוד במרווח קבוע בטבעיות מבוססת
await, קודםPeriodicTimer - אם רוצים להפעיל תקופתית
callbackקל על ה-ThreadPool,System.Threading.Timer - אם רוצים לעדכן מסך ב-UI thread של WPF,
DispatcherTimer System.Threading.Timer— ה-callback עלול לחפוף. דחיפה רשלנית של עיבוד אסינכרוני פנימה נוטה להסתבךDispatcherTimerמאפשר לגעת ב-UI ישירות, אבל אם מכניסים עיבוד כבד, קל שהוא יעצור גם את ה-UI- בהקשר של soft real-time מהמאמר הקודם, שלושת אלה אינם השחקנים הראשיים של המתנה מדויקת
בקיצור, מה שכדאי להסתכל עליו קודם הן שלוש הנקודות הבאות:
- באיזה thread / context רוצים להריץ
- האם רוצים לכתוב את גוף העיבוד ברצף עם
async/await - האם אפשר לקבל overlap של ה-callback
רק ההפרדה בין השלושה הזו כבר מקטינה מאוד בלבול.
flowchart TB
accTitle: שלוש השאלות שבודקים תחילה
accDescr: תרשים המראה שההפרדה בין שלוש שאלות — באיזה thread או context רוצים להריץ, האם רוצים לכתוב ברצף עם async ו-await, והאם אפשר לקבל overlap של callback — מקטינה מאוד בלבול בבחירת ה-timer.
q1["איפה רוצים להריץ"] --> pick["הבחירה ב-timer מתבררת"]
q2["רוצים לכתוב ברצף עם async"] --> pick
q3["אפשר לקבל overlap של callback"] --> pick
איור 3: לפני שם ה-timer, מפרידים בין שלוש השאלות האלה.
2. מבט אחד על התמונה
2.1. סקירה
flowchart LR
accTitle: תרשים ההחלטה הכולל לבחירת timer
accDescr: תרשים המראה שאם רוצים להריץ ב-UI thread בוחרים DispatcherTimer, אחרת אם רוצים לכתוב בפשטות עם async ו-await בוחרים PeriodicTimer, אחרת אם רוצים callback קל ב-ThreadPool בוחרים System.Threading.Timer, ואם לא משהו מאלה שוקלים תכנון אחר.
A["רוצים לעשות משהו במרווח קבוע"] --> B{"רוצים להריץ ב-UI thread?"}
B -- "כן" --> C["DispatcherTimer"]
B -- "לא" --> D{"רוצים לכתוב בפשטות עם async / await?"}
D -- "כן" --> E["PeriodicTimer"]
D -- "לא" --> F{"רוצים callback קל ב-ThreadPool?"}
F -- "כן" --> G["System.Threading.Timer"]
F -- "לא" --> H["לשקול תכנון אחר — Channel / BackgroundService / event / waitable timer"]
איור 4: הסדר — UI thread, כתיבה עם async, ואז callback קל — קובע את שלושת ה-timers.
בעבודה בפועל, ההסתעפות הזו מספיקה ברוב המקרים. כשמתלבטים, הכי בטוח לחתוך קודם:
עיבוד אסינכרוני — PeriodicTimer, עדכון UI — DispatcherTimer.
System.Threading.Timer נוח, אבל יש לו הרגלים סביב overlap של callback וניהול lifetime, ולכן הוא קצת קפדני כבחירה ראשונה.
2.2. טבלת החלטה
| מצב | הבחירה הראשונה | היכן זה רץ | הסיבה שזה מתאים | נקודה ראשונה לתשומת לב |
|---|---|---|---|---|
| רוצים להריץ עיבוד async כמו HTTP / DB / קובץ I/O במרווח קבוע | PeriodicTimer |
בתוך זרימת מתודת ה-async הנוכחית | אפשר לכתוב עם await, ו-stop וביטול פשוטים |
הנחה: timer אחד, consumer אחד. פיגור לא מקבל מקבילה אוטומטית |
| רוצים להריץ ב-ThreadPool heartbeat קל / שליחת מטריקות / בדיקת פקיעת cache | System.Threading.Timer |
ThreadPool | קל וקטן, מסוג callback. קל להעלות על תכנון מבוסס callback קיים | ה-callback מניח reentrancy. עלולה להיות overlap. שומרים הפניה |
| רוצים תצוגת שעון ב-WPF או עדכון UI קל במרווח קבוע | DispatcherTimer |
ה-Dispatcher (UI thread) של WPF |
אפשר לגעת ב-UI ישירות. יש עדיפות | זמן ההפעלה המדויק לא מובטח. עיבוד כבד חוסם את ה-UI |
הדיוק המחזורי הוא הנושא המרכזי, רוצים להימנע מהסתמכות על Sleep |
לא לשים את שלושת אלה במרכז | - | המטרה היא לא הרצה תקופתית של אפליקציה, אלא תכנון דיוק ההמתנה | להסתכל בצד ה-event / waitable timer |
החשוב בטבלה הזו הוא להסתכל על מקום הריצה ואופן הכתיבה, ולא על שם ה-timer. כשיש תקלה בבחירת timer, לרוב זה כי לא הסתכלו על “איפה זה רץ”, ולא בגלל שם ה-API.
flowchart TB
accTitle: נקודת המבט לבחירת timer
accDescr: תרשים המראה שבחירה לפי שם ה-API נוטה לתקלה כי לא מסתכלים איפה זה רץ, בעוד שבחירה לפי מקום הריצה ואופן הכתיבה נותנת החלטה שקשה לטעות בה.
name["בחירה לפי שם"] --> miss["תקלה כי לא בדקו איפה זה רץ"]
look["בחירה לפי מקום הריצה ואופן הכתיבה"] --> hit["בחירה שקשה לטעות בה"]
איור 5: רוב התקלות נובעות מבחירה לפי שם. מה שצריך לבדוק הוא “איפה זה רץ” ו”איך רוצים לכתוב”.
3. מה כדאי להבחין בו קודם
3.1. סוג callback, או סוג שממתין ל-tick
אם מפרידים כאן, התמונה מתבהרת בבת אחת.
System.Threading.Timerו-DispatcherTimerהם מסוג callback / eventPeriodicTimerהוא סוג שממתין ל-tick עםawait
כלומר:
- סוג callback: “צד ה-timer קורא”
PeriodicTimer: “אנחנו ממתינים ל-tick הבא”
זה ההבדל.
אם גוף העיבוד הוא async, ורוצים לקרוא את “להמתין ← לעבד ← להמתין שוב” כזרימה אחת רציפה, PeriodicTimer טבעי יותר.
מנגד:
- רוצים להעלות על תכנון קיים מבוסס callback
- גוף העיבוד קצר וסינכרוני
- פשוט רוצים בעיטה תקופתית
במצבים כאלה, System.Threading.Timer מתאים.
PeriodicTimer נוח, אבל הוא לא פותר הכול.
זה לא מניח שליחת כמה WaitForNextTickAsync בו-זמנית לאותו timer,
וגם אם מתרחשים כמה tick בזמן שלא ממתינים, הם מתקפלים לתוך אחד.
חשוב לא לטעות ולחשוב “זה ישלים פיגור מעצמו”.
flowchart TB
accTitle: סוג callback לעומת סוג שממתין ל-tick
accDescr: תרשים המראה ש-System.Threading.Timer ו-DispatcherTimer הם מסוג callback שצד ה-timer קורא, ו-PeriodicTimer הוא סוג שאנחנו ממתינים ל-tick הבא עם await.
q{"איזה סוג?"}
q -->|"סוג callback"| cb["צד ה-timer קורא"]
q -->|"סוג שממתין ל-tick"| tick["אנחנו ממתינים ל-tick הבא"]
cb --> cbex["Timer ו-DispatcherTimer"]
tick --> ptex["PeriodicTimer"]
tick -.-> flow["להמתין ← לעבד ← להמתין שוב, ברצף אחד"]
איור 6: “נקרא” או “ממתין” — ההבחנה הזו לבדה כבר מבהירה הרבה.
3.2. רץ ב-ThreadPool, או ב-UI thread
הדבר הבא שכדאי לבדוק הוא איפה זה מתבצע.
ה-callback של System.Threading.Timer רץ ב-ThreadPool, לא ב-thread שיצר אותו.
לכן הוא מתאים לעיבוד ברקע, אבל לא בהנחה שנוגעים ישירות ב-UI.
מצד שני, DispatcherTimer הוא timer UI שמשולב בתור ה-Dispatcher.
ב-WPF, הוא רץ על אותו Dispatcher, כך שאפשר לעדכן UI ישירות בתוך handler של Tick.
ההבדל הזה משמעותי מאוד.
- כדי לגעת ב-UI מ-timer של ThreadPool, צריך להחזיר במפורש ל-UI
- DispatcherTimer מקל לגעת ב-UI, אבל בתמורה צורך זמן מ-UI thread
כלומר, היתרון של DispatcherTimer הוא “אפשר לגעת ב-UI בבטחה”,
אבל זה גם אומר “אם מכניסים עיבוד כבד, זה גורר גם קלט ו-repaint”.
flowchart TB
accTitle: ההבדל במקום הריצה והמחיר שלו
accDescr: תרשים המראה שה-callback של System.Threading.Timer רץ ב-ThreadPool ודורש חזרה מפורשת כדי לגעת ב-UI, בעוד DispatcherTimer רץ ב-UI thread ומאפשר לגעת ב-UI ישירות, אך זה עולה בצריכת זמן שמכבידה על עיבוד כבד.
st["Timer: רץ ב-ThreadPool"] --> back["לגעת ב-UI דורש חזרה מפורשת"]
dt["DispatcherTimer: UI thread"] --> easy["אפשר לגעת ב-UI ישירות"]
easy --> cost["עיבוד כבד גורר קלט וציור"]
איור 7: ההבדל במקום הריצה הוא בעצם עסקת חליפין בין קלות הנגיעה ב-UI לצריכת זמן UI thread.
3.3. עיבוד מחזורי והבטחת דיוק הם נושאים שונים
הנקודה הזו חשובה כהמשך למאמר הקודם.
גם אם הניסוח “לעשות משהו במרווח קבוע” זהה,
- מבחינת האפליקציה, רוצים עיבוד תקופתי כל כמה שניות
- ברמה של מילישנייה עד כמה מילישניות, רוצים להתקרב ל-deadline ככל האפשר
אלה שני נושאים שונים.
System.Threading.Timer הוא timer קל וקל לשימוש, אבל לא כלי ייעודי לדיוק.
גם DispatcherTimer מושפע מנסיבות תור ה-Dispatcher ומהעדיפות.
גם PeriodicTimer, אם מסתכלים רק על השם, נראה “כאילו המחזור מדויק”, אבל היתרון שלו בעבודה בפועל הוא לא precision, אלא קלות כתיבת זרימת async.
לכן:
- רוצים לכתוב הרצה תקופתית של האפליקציה
- או לכוונן דיוק המתנה
בטוח יותר להפריד ביניהם כבר בהתחלה.
אם שני הנושאים האלה מתערבבים, הדיון בבחירת ה-timer נוטה לגלוש לכיוון מוזר.
flowchart TB
accTitle: הפרדה בין הרצה תקופתית לדיוק המתנה
accDescr: תרשים המראה שרצון האפליקציה לעיבוד תקופתי כל כמה שניות שונה מרצון להתקרב ל-deadline ברמת מילישנייה, ששלושת ה-timers לא מיועדים לדיוק.
same["רוצים לעשות משהו במרווח קבוע"] --> a["הרצה תקופתית של האפליקציה"]
same --> b["רוצים לכוונן דיוק המתנה"]
a --> timers["כאן נכנסים שלושת ה-timers"]
b --> design["עוברים לתכנון אופן ההמתנה עצמו"]
b -.-> note["שלושת ה-timers אינם כלי דיוק ייעודי"]
איור 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ישירות למורד הזרם - אפשר לצמצם ניהול lifetime ו-exceptions מבוסס callback
בפרט, כשגוף העיבוד:
- קורא ל-HTTP
- שואל DB
- קורא קובץ
- עושה await ל-API אסינכרוני אחר
כלומר מבוסס בעיקר על המתנת I/O — ההתאמה טובה מאוד.
שתי נקודות לתשומת לב:
- משתמשים בהנחה של timer אחד ל-consumer אחד
- קובעים בעצמכם מדיניות למקרה שהעיבוד ארוך מהמחזור
PeriodicTimer לא רץ במקביל באופן אוטומטי כדי להשלים פיגור, גם אם העיבוד הקודם התארך.
במובן הזה, זה timer ל”כתיבה טבעית של לולאת async במרווח קבוע”.
אם חושבים גם על נוחות בדיקה, נוח בשקט שאפשר להשתמש בבנאי שמקבל TimeProvider.
flowchart TB
accTitle: זרימת ההרצה התקופתית עם PeriodicTimer
accDescr: תרשים המראה שממתינים עם WaitForNextTickAsync, מריצים את גוף העיבוד ה-async, וממתינים שוב, וכשה-token מתבטל יוצאים מהלולאה, כשעיבוד שמתעכב לא רץ במקביל באופן אוטומטי.
wait["המתנה עם WaitForNextTickAsync"] --> work["הרצת גוף העיבוד ה-async"]
work --> wait
wait -.->|"ביטול ה-token"| exitl["יציאה מהלולאה ו-stop"]
work -.-> note["פיגור לא מקבל מקבילה אוטומטית"]
איור 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 מניח reentrancy
- אם העיבוד ארוך מהמרווח, ייתכן overlap
אפשר לראות את “ה-overlap האפשרית” הזו בפועל בבדיקת היחידה בדוגמה. אם מכניסים ל-timer במחזור של 50ms עיבוד שאורך 300ms, וסופרים את מספר ההרצות המקבילות המרבי, מקבלים 2 ומעלה. בגרסה עם אותה שמירה עם Interlocked.Exchange כמו למעלה, גם באותם תנאים ההרצה המקבילית המרבית נשארת 1, ובמקום זאת ה-callback שהופעל בזמן ריצה מדולג.
// קטע מתוך tests/KomuraSoft.TimerSelection.Tests/ThreadPoolTimerOverlapTests.cs
// בלי שמירה: מחזור 50ms מול עיבוד 300ms. ממתינים עד שנצפית overlap
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
לתכנן כך רגוע יותר.
flowchart TB
accTitle: overlap של callback ושמירה מפניה
accDescr: תרשים המראה שכיוון ש-System.Threading.Timer לא ממתין לסיום ה-callback הקודם, אם העיבוד ארוך מהמרווח יש overlap. שמירה עם Interlocked.Exchange משאירה הרצה מקבילית אחת ומדלגת על ההפעלה הנוכחית.
fire["הפעלת callback בכל מחזור"] --> q{"הקודם עדיין רץ?"}
q -->|"בלי שמירה"| overlap["ה-callback רץ ב-overlap"]
q -->|"עם שמירה"| skip["דילוג על ההפעלה הנוכחית"]
skip --> one["ההרצה המקבילית נשארת 1"]
איור 10: ב-timer שלא ממתין לסיום הקודם, מחליטים בעצמכם אם לאפשר overlap או לחסום עם שמירה.
עוד נקודה חשובה בשקט היא להחזיק בהפניה.
System.Threading.Timer נהיה מועמד ל-GC אם ההפניה נעלמת, גם אם הוא עדיין פעיל.
גם מיד אחרי קריאה ל-Dispose(), ייתכן שה-callback שכבר הוכנס לתור ירוץ אחר כך.
כלומר, System.Threading.Timer הוא:
- קל משקל
- מהיר
- פשוט
אבל בתמורה, זהו timer שבו אנחנו אחראים על נסיבות ה-callback.
flowchart TB
accTitle: נקודות לתשומת לב ב-lifetime של System.Threading.Timer
accDescr: תרשים המראה שיש להחזיק בהפניה ל-System.Threading.Timer, אחרת הוא נהיה מועמד ל-GC, ושגם אחרי Dispose ייתכן ש-callback שכבר בתור ירוץ מאוחר יותר.
t["System.Threading.Timer"] --> ref["המשך החזקת הפניה"]
ref -.-> gc["אם ההפניה נעלמת, מועמד ל-GC"]
t --> disp["סידור עם Dispose"]
disp -.-> late["callback שכבר בתור עלול לרוץ אחר כך"]
איור 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 נשאר קל מתחזקת.
flowchart TB
accTitle: המיקום של DispatcherPriority
accDescr: תרשים המראה ש-Background מעבד אחרי שכל שאר העיבוד הלא-idle הסתיים, נמוך מ-Input ו-Render, ולא דוחק קלט וציור מהתור, ושאם רוצים מהירות אפשר להעלות עד Normal בתמורה לשמירה על Tick קל.
tick["ה-Tick של DispatcherTimer"] --> bg["נכנס בעדיפות Background (ערך 4)"]
bg --> after["מעובד אחרי קלט וציור"]
after -.-> fit["מתאים לשעון שלא מפריע לפעולה"]
bg -.-> up["אם דוחק, לעלות ל-Normal (ערך 9)"]
up -.-> cond["בתמורה, Tick חייב להישאר קל"]
איור 12: עדיפות ה-Background כברירת מחדל ממקמת את ה-Tick במקום שלא דוחק קלט וציור.
היתרון של DispatcherTimer הוא שה-Tick מעובד על ה-Dispatcher של WPF, ולכן אפשר לגעת ב-UI ישירות.
לדוגמה, זה מתאים ל:
- תצוגת שעון
- עדכון תצוגה קל של מצב חיבור
- טריגר להערכה מחדש של Command
- עדכון קל של מספרים שמוצגים על המסך
מצבים כאלה.
עם זאת, גם כאן יש נקודה שמשנה את התמונה.
DispatcherTimer רץ ב-UI thread, ולכן אם מכניסים עיבוד כבד ל-handler של Tick, זה גורר איתו את הקלט, הציור ואף את layout ומאט אותם.
בנוסף, DispatcherTimer הוא לא כלי שמבטיח “בדיוק בזמן שנקבע”.
הוא מושפע מעבודות אחרות בתור ה-Dispatcher ומהעדיפות.
לכן, בעבודה בפועל:
- לשמור על תוכן ה-Tick קל
- להעביר I/O או CPU כבדים למקום אחר
- בסגירה, לציין במפורש את ה-lifetime עם
Stop()וביטול המנוי
מודעות לזה יוצרת יציבות.
flowchart TB
accTitle: שלוש נקודות ליציבות DispatcherTimer
accDescr: תרשים המראה שיציבות מושגת אם תוכן ה-Tick נשאר קל, עיבוד כבד של I/O או CPU מועבר לרקע, וה-lifetime מצוין במפורש עם Stop וביטול מנוי בסגירה.
dt["תפעול DispatcherTimer"] --> l1["Tick נשאר קל"]
dt --> l2["עבודה כבדה עוברת לרקע"]
dt --> l3["Stop וביטול מנוי מציינים lifetime"]
l1 -.-> why["כי זה צורך זמן מ-UI thread"]
איור 13: בתמורה לנוחות הנגיעה הישירה ב-UI, נדרש תפעול שמשאיר את ה-Tick קל ומציין lifetime.
4.4. לעיבוד מחזורי בכיוון soft real-time — כלי אחר
זו נקודת החיבור למאמר הקודם.
מה שנדון במאמר הקודם על soft real-time לא היה “בערך לרוץ כל כמה שניות”, אלא איך מצמצמים jitter במחזור ופספוסי deadline.
בהקשר הזה:
- לא להסתמך על המתנה יחסית עם
Sleep - להשתמש בגישה event-driven או ב-waitable timer
- להפריד בין fast path ל-slow path
- למדוד פיגור
אלה הנושאים המרכזיים.
לכן:
- עיבוד תקופתי async רגיל של אפליקציה ←
PeriodicTimer - callback על ThreadPool ←
System.Threading.Timer - עדכון UI ←
DispatcherTimer - דיוק מחזורי כשלעצמו ← עולם המאמר הקודם
נקי יותר להפריד את הבעיה כבר מההתחלה כך.
השאלה “רוצה לרוץ בדיוק ככל האפשר כל 1ms. איזה timer .NET מתאים?” היא בערך בחצי כבר לא שאלה של בחירת timer, אלא בעיה של אופן ההמתנה והתכנון.
flowchart TB
accTitle: חלוקת הבעיה לארבעה מראש
accDescr: תרשים המראה שעיבוד תקופתי async רגיל שייך ל-PeriodicTimer, callback על ThreadPool שייך ל-System.Threading.Timer, עדכון UI שייך ל-DispatcherTimer, ודיוק מחזורי כשלעצמו שייך לתכנון אופן ההמתנה מהמאמר על soft real-time.
p["מה רוצים לעשות"] --> a["async: PeriodicTimer"]
p --> b["callback: Timer"]
p --> c["UI: DispatcherTimer"]
p --> d["דיוק: תכנון אופן ההמתנה"]
איור 14: לפני “איזה timer”, נקי יותר לחלק כבר מההתחלה לאיזו בעיה זו שייכת.
5. anti-patterns נפוצים
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
- אי אפשר להמתין לסיום
- ניהול exceptions קשה
- צריך גם לחשוב בנפרד על overlap של ה-callback
מצב לא נוח.
שווה לפרט קצת יותר את הסיבה לכך שניהול ה-exceptions קשה. ב-async Task, ה-exception נטען על ה-Task, כך שהקורא מקבל אותו כשעושה await. ל-async void (או המקביל לו) אין Task כזה, ולכן ה-exception שנזרק נזרק מחדש ישירות אל ה-SynchronizationContext שהיה בתוקף כשהמתודה התחילה. ה-callback של System.Threading.Timer רץ על ה-ThreadPool, ושם אין SynchronizationContext. כתוצאה מכך, ה-exception הופך ל-unhandled exception ב-thread של ThreadPool, וכברירת מחדל הוא מפיל את כל ה-process. אלא אם עוטפים את פנים ה-callback ב-try / catch בעצמנו, אי אפשר לתפוס אותו בחוץ.
flowchart TB
accTitle: לאן הולך exception מ-lambda async שהועברה ל-callback
accDescr: תרשים המראה ש-TimerCallback הוא void, ולכן lambda של async שהועברה אליו הופכת ל-async void, וה-exception נזרק מחדש להקשר ה-SynchronizationContext בזמן ההתחלה, אבל ב-ThreadPool אין הקשר כזה, ולכן הוא הופך ל-unhandled exception שמפיל את ה-process כברירת מחדל.
ex["exception בתוך lambda של async"] --> void["התנהגות async void ביציאה"]
void --> ctx{"יש SynchronizationContext?"}
ctx -->|"אין ב-ThreadPool"| unh["הופך ל-unhandled exception ב-ThreadPool"]
unh --> crash["כברירת מחדל, מפיל את כל ה-process"]
ex -.-> guard["רק try / catch עצמי מונע את זה"]
איור 15: ה-exception מה-lambda הנקייה למראה של async, ללא מקום לתפוס אותו, מפיל את ה-process.
אם גוף העיבוד הוא async, קריא יותר לשקול קודם PeriodicTimer.
5.2. הכנסת עיבוד כבד ל-Tick של DispatcherTimer
מכיוון ש-DispatcherTimer מאפשר לגעת ב-UI ישירות, מתפתים לכתוב שם הכול.
אבל זה UI thread.
- עיבוד סינכרוני ארוך
- חישוב CPU כבד
- I/O חוסם
- עיבוד עם
awaitארוך שעלול להתחיל פעמיים
אם מכניסים את אלה, זה מתנגש חזיתית עם קלט וציור ה-UI.
יציב יותר לשמור על Tick קל, להעביר עבודה כבדה לרקע, ולהחזיר ל-UI רק את התוצאה הנדרשת.
5.3. לחשוב ש-PeriodicTimer משלים פיגור באופן אוטומטי
גם כאן קל לטעות.
PeriodicTimer מצוין ככלי לכתיבה נקייה של לולאת async במרווח קבוע, אבל אם העיבוד הקודם התארך, הוא לא רץ במקביל באופן אוטומטי כדי להשלים את הפיגור.
אפשר לוודא את זה גם בבדיקת היחידה בדוגמה. אם משאירים timer במחזור של 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 שמתרחשים בזמן שלא ממתינים עלולים להתקפל לתוך אחד,
- האם לדלג בפיגור
- האם מספיק להסתכל רק על העדכני ביותר
- האם רוצים בהכרח לעבד את כל הכמות
אלה צריכים להיקבע בתכנון.
flowchart TB
accTitle: קיפול ה-tick בזמן שלא ממתינים
accDescr: תרשים המראה שאם PeriodicTimer מקבל כמה tick בזמן שאף אחד לא ממתין, הם מתקפלים לתוך אחד, ואז צריך להחליט בתכנון האם לדלג בפיגור, להסתכל רק על העדכני, או לעבד את כל הכמות.
idle["כמה tick בזמן שאף אחד לא ממתין"] --> fold["מתקפלים לתוך אחד"]
fold --> q{"איך מתייחסים לפיגור?"}
q --> s1["דילוג"]
q --> s2["הסתכלות רק על העדכני"]
q --> s3["עיבוד כל הכמות"]
איור 16: ה-tick שהצטברו מתקפלים לתוך אחד. אופן השלמת הפיגור נקבע בתכנון, לא אוטומטית.
5.4. דחיית ניהול ה-stop וה-lifetime
ב-timers, יש יותר תקלות ב-stop מאשר בהפעלה.
מה שקל לפספס הוא בערך אלה:
- יוצרים
System.Threading.Timerכמשתנה מקומי בלי להחזיק בהפניה - לא עוצרים את
System.Threading.Timer, וסביבDispose()נשאר מעורפל - לא עושים
Stop()ל-DispatcherTimer, וגם לא מבטלים את מנוי ה-Tick - גם אחרי סגירת המסך, ה-timer ממשיך למשוך את ה-lifetime של האובייקט
בפרט DispatcherTimer עלול להמשיך להחזיק חי את האובייקט שהמתודה שלו קשורה (bind) אליו.
אם מתקבלת תחושה מוזרה של “ה-Window הזה אמור היה להיסגר, אבל הוא עדיין נשאר” — כדאי לחשוד כאן.
flowchart TB
accTitle: מלכודות ב-stop ובניהול lifetime
accDescr: תרשים המראה ש-Timer בלי החזקת הפניה או עם Dispose מעורפל משאיר את דרך ה-stop לא ברורה, ו-DispatcherTimer בלי Stop וביטול מנוי ממשיך להחזיק חי את האובייקט הקשור אליו, מה שגורם ל-Window שאמור היה להיסגר להישאר.
m1["Timer בלי החזקת הפניה"] --> trouble["דרך ה-stop נשארת מעורפלת"]
m2["Dispose מעורפל"] --> trouble
m3["בלי Stop ובלי ביטול מנוי"] --> keep["ממשיך להחזיק חי את הקשור"]
keep --> ghost["Window שהיה אמור להיסגר נשאר"]
איור 17: יש יותר תקלות ב-stop מאשר בהפעלה. כותבים את ה-cleanup ב-shutdown כבר מההתחלה.
6. checklist ל-code review
- אפשר להסביר אם העיבוד המחזורי הזה נכתב כעדכון UI / callback על ThreadPool / לולאת async
- האם גוף העיבוד הוא async, אבל בכל זאת נדחף בכוח לתוך timer מסוג callback
- אם משתמשים ב-
System.Threading.Timer, האם אפשר לעמוד ב-overlap של callback, או שיש שמירה מפניה - האם ל-Tick של
DispatcherTimerלא נכנס עיבוד כבד, I/O חוסם או עיבוד סינכרוני ארוך - אם משתמשים ב-
PeriodicTimer, האם נקבעה מדיניות למקרה של פיגור - האם שיטת ה-stop (
Change/Dispose/Stop) והזרימה ב-shutdown של האפליקציה ברורות - האם ההפניה ל-
System.Threading.Timerנשמרת כראוי - האם יש ביטול מנוי ו-cleanup בסגירת מסך ל-
DispatcherTimer - האם הופרד מראש אם הבעיה היא “הרצה תקופתית של האפליקציה” או “דיוק המתנה”
7. איך בוחרים בפועל
נשים כאן קני מידה לעבודה בפועל.
- רוצים לקרוא ל-API כל 30 שניות ולעדכן הגדרות ←
PeriodicTimer - רוצים לשלוח heartbeat או מטריקות קלות כל 5 שניות ←
System.Threading.Timer - רוצים תצוגת שעון או עדכון סטטוס קל ב-WPF ←
DispatcherTimer - רוצים לגעת ב-UI ישירות בכל Tick ←
DispatcherTimer - גוף העיבוד התקופתי מלא ב-
await, ורוצים לטפל בטבעיות גם ב-stop וגם ב-exceptions ←PeriodicTimer - רוצים בעיטה קטנה מבוססת callback בעלות נמוכה ←
System.Threading.Timer - דיוק מחזורי ברמת 1–5ms הוא הליבה ← לפני שלושת אלה, להסתכל על אופן ההמתנה מהמאמר הקודם
בגסות רבה, בשורה אחת:
PeriodicTimerהוא timer בשביל asyncSystem.Threading.Timerהוא timer בשביל callback על ThreadPoolDispatcherTimerהוא timer בשביל UI
עם דרך הזכירה הזו, קשה לטעות בגדול.
8. סיכום
מה שבאמת חשוב בבחירת timer ב-.NET הוא לא ההבדל בשמות, אלא שלוש הנקודות האלה:
- איפה זה רץ
- באיזו זרימה רוצים לכתוב
- איך מטפלים ב-overlap ובפיגור
בתור מדיניות, זה מספיק כדי להתמודד היטב:
- עיבוד תקופתי async ←
PeriodicTimer - callback קל על ThreadPool ←
System.Threading.Timer - עדכון UI ב-WPF ←
DispatcherTimer - דיוק הוא הליבה ← אופן המתנה אחר
ה-timers מתערבבים כי השמות שלהם דומים. אבל התפקידים לא ממש דומים.
PeriodicTimerהוא כלי לכתיבת זרימת asyncSystem.Threading.Timerהוא כלי לבעיטה תקופתית של callbackDispatcherTimerהוא כלי לעדכון תקופתי ב-UI thread
רק חשיבה נפרדת על שלושת אלה כבר מרגיעה מאוד את הקוד.
flowchart TB
accTitle: סיכום תפקידי שלושת ה-timers
accDescr: תרשים המראה ש-PeriodicTimer הוא כלי לכתיבת זרימת async, System.Threading.Timer הוא כלי לבעיטה תקופתית של callback, DispatcherTimer הוא כלי לעדכון תקופתי ב-UI thread, ולדיוק פונים לתכנון אופן ההמתנה.
t["תפקידי ה-timers"] --> pt["PeriodicTimer: async"]
t --> st["Timer: callback"]
t --> dt["DispatcherTimer: UI"]
t -.-> rt["לדיוק, לתכנון אופן ההמתנה"]
איור 18: השמות דומים, אבל התפקידים לא. עם החלוקה לשלוש הזו, קשה לטעות בגדול.
מנגד, אם זה מתערבב:
- מה שאמור להיות async נהיה דומה ל-async void
- נגיעה ישירה ב-UI וקריסה
- overlap של callback ומצב מעורפל
- גם דיון הדיוק המחזורי מתערבב
וקורים דברים מטרידים למדי בשגרה.
קודם כל, מסתכלים מ”איפה רוצים להריץ את זה”. רק זה כבר הופך את בחירת ה-timer להרבה יותר רגועה.
9. מקורות
- חבילת הדוגמאות המלאה של המאמר הזה (ספרייה, דמו, בדיקות יחידה)
- מאמר קשור: מדריך מעשי למקסום soft real-time ב-Windows רגיל — checklist ראשונה
- מאמר קשור: טבלת החלטה ל-async/await ב-C# — Task.Run ו-ConfigureAwait
- מאמר קשור: async/await וה-UI thread ב-WPF וב-WinForms
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
- DispatcherTimer Constructor (ברירת המחדל היא עדיפות Background)
- DispatcherPriority Enum
- Timer Class (System.Windows.Forms) (הדיוק הוא כ-55 מילישניות)
- Async/Await - Best Practices in Asynchronous Programming (הטיפול ב-exceptions של async void)
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה להכניס Generic Host ו-BackgroundService לאפליקציית desktop ב-.NET
בכלי Windows ובאפליקציות long-running, איך להשתמש ב-Generic Host וב-BackgroundService כדי לרכז הפעלה, עיבוד תקופתי, shutdown, לוג, הגדרות...
שיטות עבודה מומלצות ל-multithreading בפועל: מהדורת .NET — מה להחליט לפני שמוסיפים threads
כללי תכנון שמונעים מקוד multithreaded ב-.NET/C# לקרוס או להיתקע מדי פעם: להשתמש ב-Task במקום ליצור threads בעצמכם, לצמצם shared mutable s...
מה זה .NET Native AOT — ההבדל מ-JIT ומ-trimming
מהו Native AOT — לפי ההבדל מ-JIT, ReadyToRun, self-contained, single-file, trimming ו-source generator, וסיכום מעשי של המקרים שמתאימים וה...
async/await וה-UI thread ב-WPF וב-WinForms
ב-WPF וב-WinForms, לאן חוזר הקוד אחרי await, מתי להשתמש ב-Dispatcher או Invoke, מה ConfigureAwait(false) באמת עושה, ולמה .Result ו-.Wait(...
FileSystemWatcher בפועל: miss ו-duplicate
איך משתמשים ב-FileSystemWatcher בזהירות: miss, duplicate, מלכודות בקביעת סיום, rescan, atomic claim ו-idempotency.
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
תהליכון ה-UI וטיימרים
תהליכון ה-UI של WPF / WinForms, זרימות אסינכרוניות, Dispatcher ותכנון טיימרים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
בפיתוח אפליקציות Windows שכולל הרצה תקופתית, עדכון UI ועיבוד רקע, בחירת ה-timer משפיעה ישירות על איכות המימוש.
ייעוץ טכני וסקירת תכנון
בשלב שרוצים לקבוע איפה לחלק את האחריות בין PeriodicTimer ל-DispatcherTimer, אפשר לבחון זאת כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה ההבדל בין PeriodicTimer ל-System.Threading.Timer?
- ההבדל הגדול ביותר הוא ש-PeriodicTimer הוא טיפוס שממתין ל-tick עם await, ואילו System.Threading.Timer הוא טיפוס מסוג callback. את PeriodicTimer אפשר לכתוב כ"להמתין ← לעבד ← להמתין שוב" בתוך מתודת async אחת ברצף, וקל להעביר את ה-CancellationToken גם למורד הזרם. לעומת זאת, ה-callback של System.Threading.Timer רץ ב-ThreadPool, ולא ממתין לסיום ה-callback הקודם, כך שאם העיבוד ארוך יותר מהמרווח, ייתכן overlap. לעיבוד תקופתי אסינכרוני מתאים PeriodicTimer, ולהפעלה תקופתית קלה וסינכרונית של callback מתאים System.Threading.Timer.
- האם PeriodicTimer משלים באופן אוטומטי פיגור בעיבוד?
- לא. גם אם העיבוד הקודם התארך, זה לא רץ במקביל באופן אוטומטי כדי להשלים את הפיגור. גם אם מתרחשים כמה tick בזמן שלא ממתינים, הם מתקפלים לתוך אחד בלבד. לכן, האם לדלג בפיגור, להסתכל רק על העדכני ביותר, או לעבד את כל הכמות — זו החלטת תכנון. גם לא מדובר בהנחה של שליחת כמה WaitForNextTickAsync בו-זמנית לאותו timer.
- אסור להעביר lambda של async ל-System.Threading.Timer?
- עדיף להימנע מזה. TimerCallback הוא מסוג void, ולכן ה-lambda של async שמעבירים הופכת בפועל להתנהגות מסוג async void. כתוצאה מכך, הצד הקורא לא יכול לעשות await, אי אפשר להמתין לסיום, ניהול ה-exceptions נהיה קשה, וגם צריך לחשוב בנפרד על overlap של ה-callback. אם גוף העיבוד הוא async, קריא ובטוח יותר לשקול קודם PeriodicTimer.
- מתי כדאי להשתמש ב-DispatcherTimer?
- משתמשים בו כשרוצים לעדכן UI באופן תקופתי ב-WPF, כמו תצוגת שעון או תצוגת סטטוס קלה. מכיוון ש-Tick מעובד על ה-Dispatcher (UI thread) של WPF, היתרון הוא שאפשר לגעת ב-UI ישירות בתוך ה-handler. עם זאת, מכיוון שזה רץ ב-UI thread, אם מכניסים ל-Tick עיבוד כבד או I/O חוסם, זה גורר איתו גם את הקלט והציור ומאט אותם. גם אין הבטחה להפעלה בדיוק בזמן שנקבע. יציב יותר לשמור על Tick קל, להעביר עבודה כבדה לרקע, ולציין במפורש את ה-lifetime עם Stop() וביטול המנוי כשסוגרים את המסך.