למה להכניס Generic Host ו-BackgroundService לאפליקציית desktop ב-.NET

· עודכן בתאריך: · · C#, .NET, Generic Host, BackgroundService, WPF, WinForms, פיתוח Windows, תכנון

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 12 Mar 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173393)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). למה להכניס Generic Host ו-BackgroundService לאפליקציית desktop ב-.NET. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173393 https://comcomponent.com/he/blog/generic-host-backgroundservice-desktop-app/

DOI (הגרסה האחרונה)
10.5281/zenodo.22173393
DOI (הגרסה הזו)
10.5281/zenodo.22173394

כשמפתחים קצת כלי Windows או אפליקציה long-running, העיבוד שמחוץ ל-UI גדל בהדרגה. polling תקופתי, ניטור קבצים, reconnect, עיבוד תור, אתחול בהפעלה, flush ב-shutdown. בהתחלה אפשר להסתדר עם Form_Load, OnStartup או Task.Run, אבל אם זה ממשיך לגדול כך, מי מתחיל, מי עוצר ומי מנטר exceptions נהיה לא ברור.

זהו מצב שבו כדאי לקבוע קודם מי מחזיק את ה-lifetime של העיבוד, לפני עצם אופן הכתיבה של async / await. כאן עוזרים Generic Host ו-BackgroundService של .NET.

בכל הנוגע ל-async / await בצד ה-UI thread, זה מתחבר למאמרים async/await וה-UI thread ב-WPF וב-WinForms ו-טבלת החלטה ל-async/await ב-C# — Task.Run ו-ConfigureAwait. הפעם נתמקד בשכבה שמחוץ לזה — ריכוז ההפעלה וה-stop של האפליקציה כולה.

בעבודה בפועל, המקומות שנוטים להתפרק בהדרגה הם בערך אלה:

  • Task.Run צומח מכל מיני מקומות בטופס או ב-ViewModel
  • תנאי ה-stop של הלולאה שרצה ברקע מפוזרים כדגלי bool
  • ב-shutdown נשאר עיבוד שעדיין רץ, ולפעמים זה לא נסגר לגמרי
  • נקודות הכניסה של לוג / הגדרות / DI נפרדות לפי טכנולוגיה
  • מתפתה להשתמש ב-Environment.Exit לניקוי, ו-finally מדולג

המאמר הזה מניח בעיקר אפליקציות WPF / WinForms / long-running ב-Windows מ-.NET 6 ואילך, ומסביר למה Generic Host / BackgroundService יעילים בשקט, עד כמה כדאי להכניס אותם, ואיפה רשלנות גובה מחיר בהמשך.

הקוראים המיועדים אינם בהכרח מי שכבר מכיר את BackgroundService, אלא מי שנמצא בשלב שבו מתלבטים איפה למקם עיבוד שרץ ברקע ואיך לתת לו lifetime. גם מי שרואה את השם בפעם הראשונה יוכל לקרוא, כי הפרק הבא מבסס את המונחים; מי שכבר משתמש בזה יכול להתחיל ישר מטבלת ההחלטה בסעיף 2.2 ומהחלוקה בפרק 6.

הצורה שמתפרקת בהדרגהתרשים המראה ש-Task.Run שצומח מכל מיני מקומות, תנאי stop שמפוזרים כדגלי bool, עיבוד שלא נסגר ב-shutdown, ונקודות כניסה נפרדות לפי טכנולוגיה — כל אלה חוזרים לנקודה אחת: לא ברור מי מתחיל, מי עוצר ומי מנטר exceptions.Task.Run מפוזרלא ברור מי מחזיק ב-lifetimeתנאי stop כדגלי boolעיבוד שלא נסגר ב-shutdownנקודת כניסה נפרדת לכל טכנולוגיהלקבוע קודם מי מחזיק ב-lifetime

איור 1: צורות ההתפרקות שונות, אבל השורש הוא ש”מי מחזיק ב-lifetime של העיבוד” לא נקבע.

כמו כן, הקוד שמופיע במאמר הזה פורסם ב-GitHub כחבילת דוגמאות מלאה שאפשר לבנות ולהריץ (ספרייה, דמו קונסולה שממחיש הפעלה ועד graceful shutdown, ובדיקות יחידה).

generic-host-backgroundservice-desktop-app - komurasoft-blog-samples (GitHub)

מונחים שמקבעים קודם

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

  • Generic Host
    • תשתית שמטפלת יחד ב”הפעלה”, “תלויות”, “הגדרות”, “לוג” ו”stop” של אפליקציית .NET.
    • זה לא מנגנון ייחודי ל-ASP.NET Core — אפשר להשתמש בו גם בקונסולה, ב-worker ובאפליקציית desktop.
  • Host / IHost
    • הישות בפועל אחרי ה-build.
    • מפעילים אותה עם StartAsync ועוצרים עם StopAsync.
  • Hosted Service
    • עיבוד שרץ ברקע, שתלוי ב-lifetime של ה-host ומתחיל/נעצר יחד איתו.
    • מממשים IHostedService, או בדרך כלל יורשים מ-BackgroundService.
  • BackgroundService
    • עוזר מימוש שקל לכתוב מעל IHostedService.
    • כותבים את הגוף שרץ לאורך זמן בתוך ExecuteAsync, כך שקל יותר לרכז לולאת ניטור או עיבוד תקופתי.
  • lifetime
    • במאמר הזה, המשמעות היא “מתי העיבוד מתחיל, מתי הוא מסתיים, ומי אחראי לעצור אותו”.
    • זה לא סתם משך חיים, אלא ניהול מחזור חיים שכולל את אחריות ה-start ואחריות ה-stop.
  • graceful shutdown
    • לא shutdown כפוי, אלא שליחת סימן לעצירה וסיום רק אחרי שהעיבוד שבתהליך מסודר ככל האפשר.
    • לדוגמה, “לא להתחיל את המחזור הבא”, “להחליט עד לאן להעביר את התור” ו”להמתין ל-close או ל-flush” נכנסים לכאן.
  • DI
    • קיצור של Dependency Injection — דרך שבה לא כותבים את הרכבת האובייקטים התלויים ישירות בצד הקורא, אלא מקבלים אותם דרך container.
    • במאמר הזה מספיקה ההבנה של “לא ליצור logger, הגדרות או reader עם new בכל מקום, אלא להרכיב הכול בכניסה אחת”.

הדיון הזה הוא לא רק “היכרות עם המחלקה הנוחה BackgroundService”, אלא דיון על ריכוז ההפעלה וה-stop של האפליקציה כולה ב-host, והחזקת ה-lifetime של העיבוד שרץ ברקע כחלק מהתכנון. קל יותר לעקוב אם קוראים אותו כך.

הקשר בין המונחים במאמר הזהתרשים המראה ש-Generic Host הוא התשתית להפעלה, תלויות, הגדרות, לוג ו-stop, שהישות שנוצרת מ-build שלו היא IHost, שב-lifetime שלה תלוי Hosted Service, ו-BackgroundService הוא עוזר המימוש שקל לכתוב לו.Generic Host (התשתית)IHost (הישות אחרי build)Hosted ServiceBackgroundServiceהגוף שרץ לאורך זמן בתוך ExecuteAsynclifetime = אחריות ה-start וה-stop

איור 2: היררכיית המונחים. Hosted Service תלוי ב-lifetime של ה-host, ו-BackgroundService הוא עוזר המימוש שלו.

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 17, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

תוכן עניינים

  1. קודם כל, המסקנות
  2. מבט אחד על התמונה
    • 2.1. סקירה
    • 2.2. טבלת החלטה למיקום
  3. למה זה עוזר באפליקציית desktop
    • 3.1. קל להפריד בין אחריות ה-UI לעיבוד שרץ ברקע
    • 3.2. אפשר לרכז במקום אחד את כניסת ההפעלה, ה-stop וה-exceptions
    • 3.3. קל לשלב graceful shutdown בתכנון
    • 3.4. DI / לוג / הגדרות מוכנים מההתחלה
  4. מקרים שמתאימים
  5. דוגמת מבנה מינימלי (דוגמת WPF)
  6. איך מחלקים בין StartAsync / ExecuteAsync / StopAsync
    • 6.1. StartAsync
    • 6.2. ExecuteAsync
    • 6.3. StopAsync
    • 6.4. הערה לגבי .NET 10 ואילך
  7. anti-patterns נפוצים
  8. checklist ל-code review
  9. איך בוחרים בפועל
  10. סיכום
  11. מקורות

1. קודם כל, המסקנות

  • Generic Host הוא תשתית חזקה גם באפליקציית desktop להפעלה ולניהול lifetime
  • BackgroundService הוא כלי שמעלה עיבוד long-running על lifetime מנוהל, במקום סתם לזרוק ל-Task.Run
  • מה שהכי עוזר בעבודה בפועל הוא לרכז אחריות start / אחריות stop / ניטור exceptions / לוג / DI / הגדרות בתכנון אחד
  • אם מחלקים StartAsync לקצר, את הגוף שרץ לאורך זמן ל-ExecuteAsync, ואת ה-cleanup ב-shutdown ל-StopAsync, קל בהרבה לקרוא
  • אפליקציה long-running, אפליקציית tray, ניטור התקן, סנכרון תקופתי, עיבוד לפי סדר ולולאת reconnect — מתאימים במיוחד
  • מצד שני, אם הופכים גם עיבוד שרץ פעם אחת בלחיצת כפתור ל-BackgroundService, זה נהיה קצת מוגזם
  • StopAsync נוח, אבל הוא לא ביטוח מפני קריסת process או shutdown כפוי. חשוב לא לרכז בו יותר מדי cleanup

בקיצור, מה שגורם ל-Generic Host / BackgroundService לעזור באפליקציית desktop אינו “כי יש עיבוד ברקע”, אלא “כי רוצים להחזיק את ה-lifetime של העיבוד הזה כתכנון, ולא כתוספת אגבית ל-UI”.

מה שמתרכז בתכנון אחדתרשים המראה שאחריות ה-start, אחריות ה-stop, ניטור exceptions, ולוג-DI-הגדרות — אלמנטים שנוטים להתפזר — מתרכזים בתכנון אחד, וזה מה שהכי עוזר בעבודה בפועל.אחריות ה-startריכוז בתכנון אחדאחריות ה-stopניטור exceptionsלוג, DI והגדרותעלייה על lifetime מנוהל

איור 3: הערך של BackgroundService הוא שאחריות שנוטה להתפזר מתרכזת בתכנון אחד.

2. מבט אחד על התמונה

2.1. סקירה

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

התמונה הכוללת מהפעלת ה-host ועד graceful shutdownתרשים המראה שהפעלת אפליקציית desktop מפעילה build ו-StartAsync של ה-host, שמכין DI, לוג והגדרות ומפעיל את HostedService.StartAsync שמריץ את BackgroundService.ExecuteAsync עם PeriodicTimer, תור, reconnect או לולאת ניטור, בעוד המסך הראשי מוצג ומעודכן דרך Dispatcher או Invoke, וכשמשתמש מסיים או קורית שגיאה קטלנית או נקראת StopApplication מתבצע IHost.StopAsync שמודיע CancellationToken ומפעיל HostedService.StopAsync לסגירת חיבורים, flush ו-graceful shutdown.הפעלת אפליקציית desktop (WPF / WinForms)Build / StartAsync של ה-Hostהכנת DI / Logging / ConfigurationHostedService.StartAsyncBackgroundService.ExecuteAsyncPeriodicTimer / תור / reconnect / לולאת ניטורהצגת MainWindow / MainFormעדכון מצב / לוג / I/O חיצוניUI מתעדכן רק במקום הנדרש עם Dispatcher / Invokeסיום משתמש / Fatal error / StopApplicationIHost.StopAsyncהודעת CancellationTokenHostedService.StopAsyncסגירת חיבור / flush / graceful shutdown

איור 4: התמונה הכוללת מהפעלת ה-host ועד תצוגת UI, לולאת הרקע, הודעת stop ו-graceful shutdown.

עבור מי שנמצא בסביבה שלא מציגה תרשימים, נשים גם את אותה זרימה במילים.

  1. הפעלת האפליקציה (ב-WPF: App.OnStartup; ב-WinForms: Main)
  2. רישום שירותים עם Host.CreateApplicationBuilder וביצוע Build
  3. הפעלת ה-host עם IHost.StartAsync (כאן נקבעים DI, לוג והגדרות)
  4. נקרא HostedService.StartAsync של השירותים הרשומים
  5. BackgroundService.ExecuteAsync מתחיל לרוץ (הגוף של לולאת ניטור, PeriodicTimer, עיבוד תור וכדומה)
  6. הצגת ה-UI (MainWindow / MainForm). ה-worker מעדכן מאגר מצב או לוג, וה-UI קורא אותם בהקשר שלו
  7. פעולת סיום של המשתמש או שגיאה קטלנית קוראים ל-IHostApplicationLifetime.StopApplication, וממשיכים אל IHost.StopAsync
  8. ה-stop מודיע כ-CancellationToken (stoppingToken), והלולאה ב-ExecuteAsync יוצאת ממנה
  9. ב-HostedService.StopAsync סוגרים חיבורים ומבצעים flush ללוג, ומסתיימים

מה שנפוץ באפליקציות UI הוא שהאחריות מתפזרת בהדרגה בין Program.cs / App.xaml.cs / Form_Load / Closing / Task.Run / Timer / singleton סטטי.

עם הכנסת ה-host, אפשר לחלק בערך כך:

  • UI: מסך, קלט, תצוגה
  • HostedService / BackgroundService: עיבוד שרץ ברקע, ניטור, עיבוד תור, עיבוד תקופתי
  • שירותי DI: לוגיקה עסקית בפועל, חיבור חיצוני, הגדרות, לוג

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

שלוש החלוקות אחרי הכנסת ה-hostתרשים המראה שאפשר לחלק בין UI לתצוגה וקלט, HostedService לעיבוד ברקע וניטור, ושירותי DI ללוגיקה עסקית, כאשר גם עיבוד תור ועיבוד תקופתי שייכים לצד ה-HostedService.אפליקציית desktopUI: מסך, קלט, תצוגהHostedService: רקע וניטורשירותי DI: לוגיקה עסקיתגם עיבוד תור ותקופתי שייכים כאן

איור 5: מהצורה שבה האחריות מתפזרת, לחלוקה בין UI, עיבוד ברקע ועיבוד בפועל.

2.2. טבלת החלטה למיקום

מה רוצים לעשות המועמד הראשון למיקום הסיבה
אתחול קל מיד אחרי ההפעלה StartAsync משמעות ברורה כעיבוד קצר שמשתתף בהפעלה
ניטור / polling / reconnect שחי לאורך זמן ExecuteAsync קל להריץ יחד עם ה-lifetime של השירות
הודעת stop, flush או close ב-shutdown StopAsync קל לכתוב graceful shutdown יחד עם CancellationToken
הרכבת תלויות, הגדרות, לוג Host.CreateApplicationBuilder אפשר לרכז את הכניסה במקום אחד
עדכון מסך צד ה-UI פחות תקלות אם ה-worker לא נוגע ב-UI ישירות
עיבוד חד-פעמי בכל לחיצת כפתור מתודת async רגילה לרוב לא צריך HostedService
עיבוד רקע לפי סדר Channel<T> + BackgroundService קל יותר לנהל lifetime וגבול מאשר להשליך בלי מעקב

הערך של הכנסת ה-host אינו ש”אפשר להפוך משהו לאסינכרוני”, אלא שההחלטה איפה למקם אותו נעשית ברורה.

3. למה זה עוזר באפליקציית desktop

3.1. קל להפריד בין אחריות ה-UI לעיבוד שרץ ברקע

באפליקציית desktop נראה שה-UI הוא הכוכב, אבל בעבודה בפועל מה שמכביד הוא בדרך כלל מחוץ ל-UI.

לדוגמה:

  • סנכרון מצב כל 10 שניות
  • reconnect להתקן או לשרת
  • ניטור וקליטת קבצים
  • עיבוד אחורי שמצטבר בתור
  • העברת לוג ושליחת מטריקות
  • חימום cache בהפעלה

אלה לא “אירועי מסך”, אלא עיבוד שתלוי ב-lifetime של האפליקציה כולה.

אם ממקמים את זה ב-code-behind של הטופס או החלון, האחריות לעצור כשסוגרים את המסך, האחריות לתפוס exceptions, והאחריות לקבוע retry ו-backoff מתחילות להתערבב עם ענייני ה-UI.

עם BackgroundService, ההצהרה “העיבוד הזה חי כל עוד האפליקציה רצה” מקבלת ביטוי בצורת הקוד עצמו. זה חזק בשקט.

ניגוד בין דרכי המיקום של עיבוד שרץ ברקעתרשים המראה שכשעיבוד שרץ ברקע ממוקם ב-code-behind, אחריות ה-stop, ה-exceptions וה-retry מתערבבת עם ענייני ה-UI, אך כשהוא עולה על BackgroundService, ההצהרה שהוא חי כל עוד האפליקציה רצה מקבלת ביטוי בקוד.code-behindBackgroundServiceאיפה ממקמים את העיבוד שרץ ברקעstop, exceptions ו-retry מתערבבים עם UIההצהרה שזה חי תמיד מקבלת ביטוי

איור 6: אותו עיבוד, אבל אופן המיקום משנה לגמרי את אופן ההתערבבות של האחריות.

3.2. אפשר לרכז במקום אחד את כניסת ההפעלה, ה-stop וה-exceptions

גם באפליקציית desktop בלי host, אם מרכיבים בנפרד ServiceCollection, ConfigurationBuilder, LoggerFactory, אפשר להגיע לתוצאה דומה.

אבל הצורה הזו נוטה להתפזר בהדרגה.

  • ה-DI נמצא ב-Program.cs
  • ההגדרות ב-static עצמאי
  • הלוג ב-factory נפרד
  • טיפול ה-shutdown ב-ApplicationExit
  • העיבוד שרץ ברקע ב-Task.Run

גם במצב הזה, בהתחלה זה עובד. אבל אם חוזרים לבדוק אחרי כמה חודשים, מי מחזיק ב-lifetime של האפליקציה הופך לא ברור.

עם Generic Host:

  • רישום שירותים
  • טעינת הגדרות
  • תצורת לוג
  • הפעלת hosted service
  • הודעת stop
  • stop כולל דרך IHostApplicationLifetime

נכנסים לאותה מסגרת.

כלומר, קל לרכז במקום אחד את הכניסה ל”איך האפליקציה הזו מופעלת ואיך היא נעצרת”. באפליקציה long-running, זה משפיע בהמשך.

ריכוז הכניסות המפוזרות ב-hostתרשים המראה שמצורה שבה DI, הגדרות, לוג, טיפול shutdown ועיבוד רקע מפוזרים במקומות נפרדים, עוברים לצורה שבה רישום שירותים, טעינת הגדרות, תצורת לוג, הפעלת hosted service, הודעת stop ו-stop כולל נכנסים לאותה מסגרת.הכניסות מפוזרות לפי טכנולוגיהלא ברור מי מחזיק ב-lifetimeריכוז ב-Generic Hostכניסת הפעלה ו-stop במקום אחדרישום, הגדרות, לוג, הודעת stop

איור 7: גם הרכבה נפרדת עובדת, אבל השאלה אם זה נכנס לאותה מסגרת משפיעה רק אחרי כמה חודשים.

3.3. קל לשלב graceful shutdown בתכנון

עיבוד שרץ ברקע קשה יותר לעצור מאשר להתחיל. אפשר לכתוב את ה-start בשלוש שורות, אבל ב-shutdown יש פתאום הרבה יותר דברים לחשוב עליהם.

לדוגמה, בזמן ה-shutdown:

  • רוצים לבטל I/O פעיל
  • רוצים למנוע התחלה של המחזור הבא
  • רוצים להחליט עד לאן להעביר את הפריטים שנותרו בתור
  • רוצים לסגור socket או אובייקט COM
  • רוצים להמתין ל-flush של הלוג או לשמירת מצב

אם מרכזים את כל זה ב-FormClosing, זה מתערבב עם ענייני המסך והופך לקשה.

עם Host / BackgroundService, יש CancellationToken ו-StopAsync, כך שמסלול ה-stop קיים מלכתחילה.

כמובן, זה לא קסם. בקריסה או ב-kill, ייתכן ש-StopAsync בכלל לא ייקרא. עם זאת, רק העובדה שקיים תכנון של “בסיום תקין, לעצור דרך המסלול הזה” כבר משקיטה הרבה.

מסלול ה-stopתרשים המראה שסימן ה-stop מודיע דרך CancellationToken, מונע התחלה של המחזור הבא, מבטל I/O פעיל, ומוביל ל-StopAsync שסוגר וממלא flush, כשמדגישים שקריסה או kill עלולים לא לעבור במסלול הזה.סימן stopהודעת CancellationTokenלא מתחילים את המחזור הבאביטול I/O פעילStopAsync לסגירה ו-flushקריסה או kill לא עוברים כאן

איור 8: דווקא כי קשה יותר לעצור עיבוד שרץ ברקע, זה עוזר שיש מסלול stop קבוע מראש.

3.4. DI / לוג / הגדרות מוכנים מההתחלה

היתרון של Generic Host הוא לא רק BackgroundService.

  • עם Host.CreateApplicationBuilder, מוכנה תשתית DI / הגדרות / לוג
  • קל להשתמש ישירות ב-appsettings.json או במשתני סביבה
  • אפשר להשתמש ב-ILogger<T> באותה שיטה גם ב-UI וגם ב-worker
  • במידת הצורך, אפשר לרכז הגדרות עם משפחת IOptions<T>

בפרויקטי כלי Windows בפרט, נפוץ מאוד המקרה שבו “מכיוון שזה היה קטן בהתחלה, ההגדרות וה-logger שהוחזקו ברישול ב-static נהפכים בהמשך למכבידים”.

אם מלכתחילה מעלים את זה על ה-host, כשהאפליקציה קצת גדלה, יש פחות קוצר נשימה.

4. מקרים שמתאימים

Generic Host / BackgroundService יעילים במיוחד במקרים כאלה:

  • אפליקציית tray שרצה ברקע עם סנכרון תקופתי, ניטור, התראה ו-reconnect
  • אפליקציה שמתחברת להתקן / מצלמה / socket עם שמירת חיבור, ניטור, retry וקבלת מצב
  • כלי אינטגרציית קבצים עם ניטור, תור קליטה ועיבוד לפי סדר
  • מניעת התנפחות כלים פנים-ארגוניים קטן בהתחלה, אבל צפויים להצטבר הגדרות, לוג ו-I/O חיצוני
  • אפליקציה שאיכות ה-shutdown שלה חשובה לא רוצים להשאיר מצב חצי-מוגמר בסגירה

מצד שני, יש גם מקרים שאין צורך להכניס host מיד:

  • כלי קטן שמופעל פעם אחת, מעבד פעם אחת ומסתיים
  • מסך שכמעט אין בו עיבוד ברקע, ומסתיים באירועי UI בלבד
  • כלי עזר פנים-ארגוני קטן באמת, שהתלויות וההגדרות שלו כמעט לא גדלות

ה-host הוא לא “חובה”. אבל ברגע שרואים שני עיבודי רקע ומעלה, כדאי מאוד לשקול אותו ברצינות. זה עולה הרבה פחות מאשר לנקות אחר כך Task.Run מפוזרים.

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

איור 9: ה-host אינו חובה, אבל הכנסתו כשעיבודי הרקע מתחילים לגדול זולה יותר מהניקוי המאוחר.

5. דוגמת מבנה מינימלי (דוגמת WPF)

בתור דוגמה, נכתוב מבנה מינימלי שמפעיל host ב-WPF ומריץ BackgroundService שקורא מצב חיצוני כל 5 שניות. גם ב-WinForms, אופן החשיבה כמעט זהה — רק נקודת הכניסה משתנה ל-Main / ApplicationContext.

הקוד יופיע מחולק לשלושה, אז נציג קודם את מבנה הקבצים.

קובץ תוכן מוצג
App.xaml.cs יצירת ה-host, רישום DI, StartAsync / StopAsync, הצגת MainWindow 5.1
DevicePollingBackgroundService.cs לולאת רקע שקוראת מצב כל 5 שניות 5.2
StatusStore.cs המצב שמשותף בין ה-worker ל-UI. גם רשומת DeviceStatus נמצאת כאן 5.3
IDeviceStatusReader.cs / DeviceStatusReader.cs העיבוד שבפועל קורא מצב חיצוני הושמט מהמאמר. המימוש נמצא בדוגמת GitHub
MainWindow.xaml / MainWindow.xaml.cs המסך. קורא את StatusStore ומציג הושמט מהמאמר. קוד מסך רגיל של WPF

הדוגמה ב-GitHub היא צורה שמריצה את המבנה הזה בקונסולה, וכוללת בנוסף למימוש של BackgroundService ו-StatusStore גם דמו שעובר מהפעלה ועד graceful shutdown, ובדיקות יחידה.

5.1. App.xaml.cs

using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DesktopHostSample;

public partial class App : Application
{
    private IHost? _host;

    protected override async void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        HostApplicationBuilder builder = Host.CreateApplicationBuilder(e.Args);

        builder.Services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(15);
        });

        builder.Services.AddSingleton<MainWindow>();
        builder.Services.AddSingleton<StatusStore>();
        builder.Services.AddScoped<IDeviceStatusReader, DeviceStatusReader>();
        builder.Services.AddHostedService<DevicePollingBackgroundService>();

        _host = builder.Build();

        await _host.StartAsync();

        MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        if (_host is not null)
        {
            await _host.StopAsync();
            _host.Dispose();
        }

        base.OnExit(e);
    }
}

יש שלוש נקודות מרכזיות לצורה הזו:

  1. מפעילים את ה-host לפני הצגת ה-UI
  2. ב-shutdown, עושים await מפורש ל-StopAsync
  3. מרכזים DI, hosted service ו-shutdown timeout בכניסה

ShutdownTimeout הוא הגבול העליון כברירת מחדל שבו IHost.StopAsync ממתין לטיפול ה-shutdown. ברירת המחדל משתנה לפי גרסה: .NET 6 — 5 שניות, .NET 7 ואילך — 30 שניות. הסיבה שכתוב כאן 15 שניות היא שקובעים את הגבול בעצמנו בהתאם לטיפול ה-shutdown האיטי ביותר. קנה המידה הוא בערך “ה-timeout של ה-I/O הפעיל + הזמן שלוקח ל-close / flush”, עם קצת רזרבה. אם קצר מדי, ה-flush נחתך באמצע; אם ארוך מדי, זה נראה כ”אפליקציה שלא נסגרת” — לכן קביעה אחת מודעת, ולא השארה בברירת המחדל, מקטינה תקלות.

איך קובעים את ShutdownTimeoutתרשים המראה שלא משאירים את הגבול על ברירת המחדל, אלא מזהים את טיפול ה-shutdown האיטי ביותר, מוסיפים ל-timeout של ה-I/O את זמן ה-flush, וקובעים ערך בעצמנו, כשגבול קצר מדי חותך את ה-flush וגבול ארוך מדי נראה כאפליקציה שלא נסגרת.זיהוי טיפול ה-shutdown האיטי ביותרהוספת זמן flush ל-timeout ה-I/Oקביעת גבול עצמאיתקצר מדי: ה-flush נחתךארוך מדי: נראה כאפליקציה שלא נסגרת

איור 10: לא משאירים את ShutdownTimeout בברירת המחדל — קובעים אותו מתוך חישוב לאחור מטיפול ה-shutdown האיטי ביותר.

הפיכת OnExit ל-async דורשת קצת תשומת לב בגלל אילוצי מסגרת ה-UI, אבל יש משמעות רבה בכתיבה מפורשת של הזרימה “ב-shutdown, עוצרים את ה-host”.

5.2. BackgroundService

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace DesktopHostSample;

public sealed class DevicePollingBackgroundService(
    IServiceScopeFactory scopeFactory,
    StatusStore statusStore,
    ILogger<DevicePollingBackgroundService> logger) : BackgroundService
{
    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Device polling service is starting.");
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Device polling loop started.");

        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                using IServiceScope scope = scopeFactory.CreateScope();
                IDeviceStatusReader reader =
                    scope.ServiceProvider.GetRequiredService<IDeviceStatusReader>();

                DeviceStatus status = await reader.ReadAsync(stoppingToken);
                statusStore.Update(status);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Device polling failed.");
            }
        }

        logger.LogInformation("Device polling loop finished.");
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Device polling service is stopping.");
        await base.StopAsync(cancellationToken);
        logger.LogInformation("Device polling service stopped.");
    }
}

החשוב כאן הוא לכתוב את ExecuteAsync בפשטות כ“לולאת while מנוהלת”.

  • המחזור עם PeriodicTimer
  • ה-stop עם stoppingToken
  • ה-exceptions ללוג
  • אם נדרשת תלות scoped, פותחים scope בכל פעם

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

הצורה של לולאת while מנוהלתתרשים המראה שממתינים למחזור עם PeriodicTimer, פותחים scope לתלות scoped וקוראים מצב שמעדכן את המאגר, וממשיכים בלולאה, כשכשל נרשם ללוג וממשיך, ו-stoppingToken יוצא מהלולאה.כשלstoppingTokenהמתנה למחזור עם PeriodicTimerפתיחת scope וקבלת תלותקריאת מצב ועדכון המאגררישום ללוג והמשךיציאה מהלולאה וסיום

איור 11: ExecuteAsync הוא “לולאת while מנוהלת”. המחזור, ה-stop, ה-exceptions וה-scope נקראים במקום אחד.

5.3. שיתוף מצב — לא חיבור ישיר ל-UI

אם נוגעים ישירות באובייקטי UI מה-worker, בעיית ה-UI thread חוזרת בדיוק שם.

לכן, קודם כול:

  • ה-worker מעדכן מאגר מצב או שכבת מסרים
  • ה-UI קורא / משקף את המצב הזה בהקשר שלו עצמו

ההפרדה הזו בטוחה יותר.

אפשר להפוך את StatusStore לשכבת שיתוף דקה כזו, לדוגמה:

namespace DesktopHostSample;

public sealed class StatusStore
{
    private readonly object _gate = new();
    private DeviceStatus _current = DeviceStatus.Empty;

    public DeviceStatus Current
    {
        get
        {
            lock (_gate)
            {
                return _current;
            }
        }
    }

    public void Update(DeviceStatus next)
    {
        lock (_gate)
        {
            _current = next;
        }
    }
}

public sealed record DeviceStatus(string Message)
{
    public static readonly DeviceStatus Empty = new("No Data");
}

אם נדרשת התראה מיידית ל-UI, משתמשים ב-Dispatcher / BeginInvoke / אירוע / messenger וכדומה. עם זאת, פחות מתערבב אם האחריות הזו נשמרת בגבול ה-UI.

הפרדת שיתוף המצב מחיבור ישיר ל-UIתרשים המראה שה-worker לא נוגע ישירות באובייקטי UI, אלא מעדכן מאגר מצב או שכבת מסרים, וה-UI קורא ומשקף בהקשר שלו, כשאחריות ההתראה המיידית נשמרת בגבול ה-UI.worker (לולאת רקע)עדכון מאגר המצבUIקריאה בהקשר שלו עצמוהתראה מיידית — באחריות גבול ה-UI

איור 12: שכבת שיתוף דקה בין ה-worker ל-UI מונעת את חזרתה של בעיית ה-UI thread.

6. איך מחלקים בין StartAsync / ExecuteAsync / StopAsync

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

6.1. StartAsync

StartAsync הוא המקום לעיבוד קצר שמשתתף בהפעלה.

מתאים לו:

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

לא מתאים לו:

  • warm-up שלוקח עשרות שניות
  • לולאה אינסופית
  • גוף עיבוד עם הרבה I/O כבד

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

איך מבחינים מה שייך ל-StartAsyncתרשים המראה שעיבוד קצר שמשתתף בהפעלה כמו לוג הפעלה מתאים ל-StartAsync, אך אם זה לא קצר, כמו warm-up ארוך, לולאה אינסופית או I/O כבד, זה שייך לצד הגוף כמו ExecuteAsync, שאם הוא כבד גורם לעלייה איטית של האפליקציה.כןלאעיבוד קצר שמשתתף בהפעלה?שייך ל-StartAsyncשייך לגוף כמו ExecuteAsyncאם זה כבד, העלייה נראית איטית

איור 13: StartAsync הוא המקום ל”סימן ה-start”, לא המקום לעיבוד כבד.

6.2. ExecuteAsync

ExecuteAsync הוא הגוף של ה-lifetime של השירות.

מתאים לו:

  • polling
  • לולאת ניטור
  • לולאת reconnect
  • consumer שקורא Channel<T>
  • עיבוד תקופתי
  • עיבוד כללי ש”חי עד ה-stop”

יש שלושה טריקים כאן:

  1. להעביר את CancellationToken מההתחלה ועד הסוף
  2. לוודא שהלולאה לא מתה בשקט עקב exception
  3. לא להוסיף retry או backoff באופן אילתורי מדי

BackgroundService נוח, אבל אם משאירים אותו כמו שהוא, הוא עלול להפוך ל”לולאה ענקית שבולעת הכול”. קריא יותר לחתוך את העיבוד עצמו לשירות נפרד, ולהשאיר את ExecuteAsync מוקדש לניהול lifetime ולתזמור.

שלושת הטריקים לשמור ExecuteAsync כגוףתרשים המראה ששלושת הטריקים הם להעביר את ה-token עד הסוף, לא לתת ללולאה למות בשקט, ולא להגדיל retry באופן אילתורי, כשהתפקיד המרכזי של ExecuteAsync הוא ניהול lifetime.ExecuteAsyncהעברת ה-token עד הסוףלא מת בשקטלא מגדילים retry יתר על המידהמוקדש לניהול lifetime

איור 14: שלושת הטריקים לשמור על ExecuteAsync כמקום לניהול lifetime, ולא כלולאה ענקית.

6.3. StopAsync

StopAsync הוא המקום לcleanup בסיום תקין.

מתאים לו:

  • לוג stop
  • ביטול timer / subscription / ניטור
  • סידור משאבים שרוצים לסגור / לעשות להם flush במפורש
  • המתנת סיום דרך base.StopAsync

עם זאת, חשוב לא לצפות מ-StopAsync להכול.

  • ה-process קרס
  • shutdown כפוי
  • נהרג בצד מערכת ההפעלה

בסוגי סיום כאלה, ייתכן שהוא בכלל לא יעבור.

לכן:

  • להשלים שמירה קבועה בכמויות קטנות בזמן רגיל, ככל האפשר
  • לא לתכנן כך שהעקביות מושגת רק בזמן ה-shutdown
  • לשמור על ה-cleanup כ-idempotent

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

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

איור 15: StopAsync הוא עזר לסיום תקין, לא ביטוח מפני סיום חריג.

6.4. הערה לגבי .NET 10 ואילך

כשינוי שובר (breaking change) ב-.NET 10 (שיצא בנובמבר 2025), ההתנהגות של BackgroundService.ExecuteAsync השתנתה כך שכל הפונקציה רצה כ-task ברקע.

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

עם זאת, גם ככה מבחינת תכנון:

  • עיבוד קצר שמשתתף בהפעלה ← StartAsync
  • הגוף שרץ לאורך זמן ← ExecuteAsync

קריא יותר לחלק כך.

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

הבדל ההתנהגות של ExecuteAsync לפי גרסהתרשים המראה שב-.NET 9 ומטה, החלק הסינכרוני שלפני ה-await הראשון עלול לחסום את ההפעלה, בעוד ב-.NET 10 ואילך כל הפונקציה רצה ברקע, אך בשני המקרים קריא יותר להפריד את העיבוד הקצר ל-StartAsync..NET 9 ומטה.NET 10 ואילךמהו יעד ה-.NET?החלק הסינכרוני עלול לחסום הפעלהכל הפונקציה רצה ברקעעיבוד קצר של ההפעלה שייך ל-StartAsync

איור 16: ההתנהגות משתנה בין הגרסאות, אבל התכנון של הפרדה בין StartAsync ל-ExecuteAsync לא משתנה.

7. anti-patterns נפוצים

7.1. תחילת לולאה אינסופית ב-Window_Loaded / Form_Shown

בהתחלה זה נוח. אבל אחריות ה-stop וה-exceptions נדבקת חזק לצד ה-UI.

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

7.2. השלכת Task.Run בלי מעקב

Task.Run עצמו לא רע. מה שרע הוא שאין בעלים ל-lifetime ול-exceptions.

בפרט אם מתחילים עיבוד רקע עם Task.Run(async () => { while (...) { ... } }),

  • מתי זה נגמר
  • מי ממתין
  • איך רואים את ה-exceptions
  • כמה זמן ממתינים ב-shutdown

נהיה לא ברור.

רק העלאה על BackgroundService כבר מקלה מאוד על הריכוז.

7.3. נגיעה ישירה ב-UI מתוך BackgroundService

זו מוקש. בעיית ה-UI thread ובעיית ה-lifetime מתערבבות בבת אחת.

ה-worker לא נוגע ישירות ב-UI, אלא:

  • מצב
  • אירוע
  • מסר
  • queue

בטוח יותר לשים גבול דרך אחד מאלה.

7.4. הרכזת שמירה חשובה רק ב-StopAsync

StopAsync עוזר בסיום תקין, אבל הוא לא המשפט האחרון.

תכנון שבו שומרים רק ב-shutdown, עושים flush רק ב-shutdown, והעקביות מתקיימת רק ב-shutdown — נשבר בקריסה.

7.5. שימוש ב-Environment.Exit ברישול למרות שיש host

גם זה נפוץ.

אם קוראים ל-Environment.Exit מתוך “נמאס, בואו פשוט נפיל”, זה חותך בעצמו את מסלול ה-graceful shutdown שה-host מחזיק.

אם רוצים לסיים את הכול עקב שגיאה קטלנית, פשוט יותר להשתמש קודם ב-IHostApplicationLifetime.StopApplication() ולעבור דרך המסלול הרשמי ל-stop.

שני המסלולים לסיום כוללתרשים המראה שאם מפילים עם Environment.Exit, חותכים בעצמם את מסלול ה-graceful shutdown שה-host מחזיק, ואילו אם רוצים לסיים בגלל שגיאה קטלנית עדיף להשתמש ב-StopApplication שעובר עד StopAsync בדרך הרשמית.Environment.ExitStopApplicationרוצים לסיים עקב שגיאה קטלניתבאיזה אופן להפיל?חיתוך מסלול ה-graceful shutdownהמסלול הרשמי ל-stopמגיע עד StopAsync ומסתיים

איור 17: אם משתמשים ב-host אבל מפילים עם Environment.Exit, חותכים בעצמכם את מסלול ה-stop שהכנתם.

8. checklist ל-code review

בסקירת אפליקציית desktop שמשתמשת ב-Generic Host / BackgroundService, ברור יותר לבדוק את הבאים בסדר:

  • האם העיבוד תלוי ב-lifetime של האפליקציה, או סתם עיבוד אירוע UI
  • האם אחריות ההפעלה מחולקת נכון בין StartAsync, ExecuteAsync ו-StopAsync
  • האם StartAsync לא נעשה כבד מדי
  • האם ExecuteAsync מעביר את CancellationToken עד הסוף
  • האם תלות scoped לא מוחזקת ישירות מה-hosted service
  • האם ה-worker לא נוגע ישירות באובייקטי UI
  • האם exceptions לא נבלעים בשקט
  • האם לולאת retry לא הופכת לתדירות אינסופית וגבוהה
  • האם יש גבול לזמן ההמתנה ב-shutdown
  • האם לא מעורבב סיום שמניח Environment.Exit או kill של ה-process

עם ה-checklist הזו, ההבדל בין “בינתיים הכנסתי host” לבין “ריכזתי את ה-lifetime כתכנון” נהיה ברור הרבה יותר.

9. איך בוחרים בפועל

מה רוצים לעשות הבחירה הראשונה
להרכיב DI / לוג / הגדרות של האפליקציה כולה Host.CreateApplicationBuilder
להריץ לולאה שרצה ברקע BackgroundService
לרוץ במרווח קבוע PeriodicTimer + BackgroundService
להעביר עיבוד אחורי לפי סדר Channel<T> + BackgroundService
להשתמש בשירות scoped IServiceScopeFactory.CreateScope()
להודיע על סיום תקין לכל האפליקציה IHostApplicationLifetime.StopApplication()
עדכון UI בצד ה-UI, עם Dispatcher / Invoke
פעולת מסך חד-פעמית מתודת async רגילה
שליטה קפדנית ב-lifetime בהפעלה לשקול IHostedLifecycleService

10. סיכום

הסיבה להכניס Generic Host / BackgroundService לאפליקציית desktop אינה “כי רוצים לכתוב בסגנון שדומה ל-Web”.

מה שבאמת עוזר הם שלושת אלה:

  1. אפשר לרכז את אחריות ההפעלה וה-stop במקום אחד
  2. אפשר להחזיק את ה-lifetime של העיבוד שחי לאורך זמן כתכנון
  3. אפשר לטפל ב-graceful shutdown מהכניסה, ולא כתוספת מאוחרת

כלי Windows ואפליקציות long-running, גם אם קטנים בהתחלה, מצטברים בהדרגה ניטור, סנכרון, reconnect, תור, לוג והגדרות. אם מפעילים את זה כתוספת אגבית לקוד ה-UI, זה נהיה קשה בשקט בהמשך.

מצד שני:

  • ה-UI הוא ה-UI
  • העיבוד שרץ ברקע הוא hosted service
  • העיבוד בפועל הוא שירות DI
  • ה-shutdown הוא StopAsync ו-CancellationToken

רק החלוקה הזו כבר מרכזת הרבה.

החלוקה המסכמתתרשים המראה שהחלוקה בין UI כ-UI, עיבוד רקע כ-hosted service, עיבוד בפועל כשירות DI, ו-shutdown עם StopAsync ו-CancellationToken, מרכזת הרבה בלי מאמץ מיוחד.האפליקציה כולהה-UI הוא ה-UIעיבוד רקע הוא hosted serviceעיבוד בפועל הוא שירות DIה-shutdown הוא StopAsync ו-token

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

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

אם אתם תקועים בכלי Windows או באפליקציה long-running — בהמרה ל-BackgroundService, בתכנון הפעלה/stop, בלולאת ניטור, בסידור lifetime של COM / socket / ניטור קבצים, או בבידוד תקלות בזמן shutdown — אפשר להתייעץ אצלנו החל מסקירת תכנון וקיבוע מדיניות.

11. מקורות

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

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

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

שאלות נפוצות

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

אפשר להשתמש ב-Generic Host גם באפליקציית desktop?
כן. Generic Host אינו מנגנון ייחודי ל-ASP.NET Core — אפשר להשתמש בו גם בקונסולה, ב-worker ובאפליקציות desktop של WPF / WinForms, כתשתית שמטפלת יחד בהפעלה, תלויות, הגדרות, לוג ו-stop. הוא מתאים במיוחד לאפליקציה long-running, אפליקציית tray, ניטור התקן, סנכרון תקופתי, עיבוד לפי סדר ולולאת reconnect.
בשביל מה משתמשים ב-BackgroundService?
זהו כלי להעלות עיבוד long-running על lifetime מנוהל, במקום סתם לזרוק אותו ל-Task.Run. זה עוזר שקל לכתוב מעל IHostedService, ומאפשר לכתוב את גוף לולאת הניטור או העיבוד התקופתי בתוך ExecuteAsync. ההצהרה "העיבוד הזה חי כל עוד האפליקציה רצה" מקבלת ביטוי בצורת הקוד עצמו, ואפשר לרכז את אחריות ה-start, אחריות ה-stop, ניטור exceptions, לוג, DI והגדרות בתכנון אחד.
איך מחלקים בין StartAsync, ExecuteAsync ו-StopAsync?
קל יותר לקרוא אם מחלקים כך: אתחול קצר שמשתתף בהפעלה — StartAsync; הגוף שרץ לאורך זמן — ExecuteAsync; והתראת stop, flush או close —‏ StopAsync. מצד שני, עיבוד שרץ פעם אחת רק בלחיצת כפתור מספיק שיהיה מתודת async רגילה — הפיכת הכול ל-BackgroundService נהיית מוגזמת.
מותר לרכז את כל טיפול ה-shutdown ב-StopAsync?
לא רצוי. StopAsync נוח, אבל הוא לא ביטוח מפני קריסת process או shutdown כפוי, ולכן חשוב לא לרכז בו יותר מדי cleanup. מתכננים graceful shutdown (ביטול I/O פעיל, עצירת המחזור הבא, מדיניות לטיפול בפריטים שנותרו בתור, סגירת חיבור, flush ללוג) באמצעות CancellationToken ו-StopAsync, אבל צריך גם הנחה נפרדת שלא נשברת גם ב-shutdown חריג.

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג