למה להשתמש ב-Generic Host וב-BackgroundService של ‏.NET באפליקציית desktop

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

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

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

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

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

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

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

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

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

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

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

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

מונחים שמסדרים קודם

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

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

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

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

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

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

המאמר הזה מסודר סביב הסיבה להשתמש ב-Generic Host וב-BackgroundService של ‏.NET באפליקציית desktop של WPF או WinForms. Generic Host הוא הבסיס להפעלה שמנהל יחד הזרקת תלויות (‏DI), לוגים ותהליך עצירה, ו-BackgroundService הוא מימוש נוח לכתיבה של Hosted Service, שמעביר לולאה תדירה מ’זריקה בלי מעקב’ של Task.Run לניהול-חיים מבוקר. עיבוד תדיר או מסודר-סדר שמשתמש ב-PeriodicTimer או ב-Channel מקבל את איתות העצירה עד StopAsync דרך CancellationToken, ותקרת ההמתנה של ה-graceful shutdown מוגדרת דרך ‏HostOptions.ShutdownTimeout. כשרוצים לעצור את הכול בעקבות שגיאה קטלנית, הדרך התקנית היא להשתמש ב-‏IHostApplicationLifetime, וסיום כפוי עם Environment.Exit חותך את המסלול הזה עצמו.

מפת הידע של Generic Host ושל BackgroundServiceתרשים שמראה שאפליקציית desktop הופכת את Generic Host לבסיס ההפעלה שלה; ש-BackgroundService, כ-Hosted Service, מעביר עיבוד תדיר לניהול-חיים מבוקר עם CancellationToken ו-ShutdownTimeout, לעבר graceful shutdown; ו-ש-Environment.Exit לא מתיישב עם המסלול הזה.מממש אתמשתמש במשתמש במשתמש במשתמש במשתמש במשתמש במשתמש במשתמש במחייבמוגדר באמצעותאינו מתיישב עםמענה מומלץ למשתמש בשימוש לא מומלץ למחייבמחייבGeneric HostBackgroundServiceHosted Service (IHostedService)הזרקת תלויות (DI)WPFWindows FormsPeriodicTimerCancellationToken(.NET)Channelgraceful shutdownHostOptions.ShutdownTimeoutEnvironment.ExitIHostApplicationLifetimeIHostedLifecycleServiceTask.Run‏.NET (מ-Core ואילך)

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

תוכן עניינים

  1. קודם כול, המסקנה (במשפט אחד)
  2. קודם כול, לסדר בדף אחד
    • 2.1. התמונה הכוללת
    • 2.2. טבלת ההחלטה למיקום
  3. למה זה עוזר באפליקציית desktop
    • 3.1. קל להפריד בין אחריות ה-UI לעיבוד שרץ ברקע
    • 3.2. אפשר לרכז במקום אחד את כניסת ההפעלה, העצירה והחריגות
    • 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. אנטי-דפוסים נפוצים
  8. רשימת בדיקה לסקירת קוד
  9. חלוקה גסה בין המצבים
  10. סיכום
  11. מקורות

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

  • Generic Host הוא תשתית חזקה גם באפליקציית desktop להפעלה ולניהול lifetime
  • ‏BackgroundService הוא כלי שמעלה “עיבוד שחי הרבה זמן” על מחזור חיים מנוהל, במקום סתם לזרוק ל-Task.Run
  • מה שהכי עוזר בעבודה בפועל הוא לרכז אחריות ההתחלה / אחריות העצירה / ניטור חריגות / לוג / DI / הגדרות בתכנון אחד
  • אם מחלקים StartAsync לקצר, את הגוף שרץ הרבה זמן ל-ExecuteAsync, ואת הסידור בסיום ל-StopAsync, קל בהרבה לקרוא
  • אפליקציה שרצה ברקע, אפליקציית tray, ניטור התקן, סנכרון תקופתי, עיבוד עוקבין אחר סדר ולולאת חיבור מחדש — מתאימים במיוחד
  • מצד שני, אם הופכים גם עיבוד שרץ פעם אחת בלחיצת כפתור ל-BackgroundService, זה נהיה קצת מוגזם
  • StopAsync נוח, אבל הוא לא ביטוח מפני קריסת תהליך או סיום כפוי. חשוב לא לרכז בו יותר מדי סידור אחרון

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

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

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

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

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

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

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

איור 4: התמונה הכוללת מהפעלת ה-host ועד תצוגת UI, לולאת הרקע, הודעת עצירה ו-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. העצירה מודיעה כ-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 / חיבור מחדש שחי הרבה זמן ExecuteAsync קל להריץ יחד עם מחזור חיי השירות
הודעת עצירה, flush או close בסיום StopAsync קל לכתוב graceful shutdown יחד עם CancellationToken
הרכבת תלויות, הגדרות, לוג Host.CreateApplicationBuilder אפשר לרכז את הכניסה במקום אחד
עדכון מסך צד ה-UI פחות תקלות אם ה-worker לא נוגע ב-UI ישירות
עיבוד חד-פעמי בכל לחיצת כפתור שיטת async רגילה לרוב לא צריך HostedService
עיבוד רקע עוקב סדר Channel<T> +‏ BackgroundService קל יותר לנהל מחזור חיים וגבול מאשר להשליך בלי מעקב

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

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

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

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

לדוגמה:

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

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

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

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

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

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

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

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

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

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

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

עם Generic Host:

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

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

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

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

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

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

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

לדוגמה, בזמן הסיום:

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

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

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

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

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

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

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

מצד שני, יש גם מקרים שאין צורך להכניס 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. בסיום, עושים await מפורש ל-StopAsync
  3. מרכזים DI, hosted service ו-shutdown timeout בכניסה

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

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

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

הפיכת OnExit ל-async דורשת קצת תשומת לב בגלל אילוצי מסגרת ה-UI, אבל יש משמעות רבה בכתיבה מפורשת של הזרימה “בסיום, עוצרים את ה-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
  • העצירה עם stoppingToken
  • החריגות ללוג
  • אם נדרשת תלות scoped, פותחים scope בכל פעם

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

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

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

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

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

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

  • ה-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.

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

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

6.1. StartAsync

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

מתאים לו:

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

לא מתאים לו:

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

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

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

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

6.2. ExecuteAsync

ExecuteAsync הוא הגוף של מחזור חיי השירות.

מתאים לו:

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

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

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

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

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

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

6.3. StopAsync

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

מתאים לו:

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

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

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

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

לכן:

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

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

הטווח שכדאי לצפות ממנו מ-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 נכנס לתמונה. זו נקודה שקטה שמשפיעה כשהאפליקציה שרצה ברקע מתחילה “להתעבות”.

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

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

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

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

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

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

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

Task.Run עצמו לא רע. מה שרע הוא שאין בעלים למחזור החיים ולחריגות.

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

  • מתי זה נגמר
  • מי ממתין
  • איך רואים את החריגות
  • כמה זמן ממתינים בסיום

נהיה לא ברור.

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

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

זו מוקש. בעיית ת’רד ה-UI ובעיית מחזור החיים מתערבבות בבת אחת.

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

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

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

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

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

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

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

גם זה נפוץ.

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

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

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

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

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

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

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

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

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

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

10. סיכום

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

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

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

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

מצד שני:

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

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

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

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

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

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

11. מקורות

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

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

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

שאלות נפוצות

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

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

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג