خدمات پسزمینه در .NET 10: چگونه ExecuteAsync بالاخره روی نخ جداگانهای اجرا میشود
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۹/۰۲ ۱۰:۰۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
IHostedService و کلاس پایه انتزاعی BackgroundService. اما اگر در حال ارتقا به .NET 10 هستید، یک بهبود کلیدی در زمینه نخها (Threading) وجود دارد که زندگی شما را به عنوان توسعهدهنده آسانتر میکند.IHostedServiceو الگوهای صحیحIHostedService، که بخشی از میزبان عمومی (Generic Host) در .NET است، پایهایترین ابزار برای مدیریت خدمات پسزمینه محسوب میشود. این رابط دو متد کلیدی تعریف میکند: StartAsync(CancellationToken) که هنگام راهاندازی برنامه فراخوانی میشود، و StopAsync(CancellationToken) که هنگام توقف برنامه اجرا میگردد. نکته حیاتی این است که StartAsync باید فوراً تکمیل شود تا راهاندازی برنامه بلاک نشود.Timer، کار را به نخهای استخر نخ (Thread Pool) واگذار میکند و راهاندازی را بلاک نمیکند:public class MyBackgroundService : IHostedService
{
private Timer? _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
// تایمر کار را راهاندازی میکند و فوراً برمیگردد
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Dispose();
return Task.CompletedTask;
}
private void DoWork(object? state)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
}
}StartAsync بلافاصله Task.CompletedTask برمیگرداند، در حالی که کار واقعی روی نخهای جداگانهای از استخر نخها اجرا میشود. این الگو برای وظایف دورهای ایدهآل است و از بلاک شدن جلوگیری میکند.StartAsync(هشدار!)while متد StartAsync را بلاک میکند و برنامه را در حین راهاندازی معلق نگه میدارد:public class MyBackgroundService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
}
return Task.CompletedTask; // هرگز به اینجا نمیرسد!
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}StartAsync قرار ندهید. همیشه آنها را به نخهای جداگانه واگذار کنید. این اشتباه نه تنها راهاندازی را کند میکند، بلکه در سناریوهای واقعی مانند کانتینرهای Docker یا Azure App Service، میتواند منجر به شکستهای زمانی (Timeouts) شود. برای جلوگیری، همیشه از ابزارهایی مانند Task.Run یا تایمرها استفاده کنید.BackgroundService: سادگی با یک تله پنهان در .NET 9BackgroundService یک کلاس پایه انتزاعی است که IHostedService را برای شما پیادهسازی میکند. شما فقط نیاز به بازنویسی متد ExecuteAsync دارید، که این کار را برای وظایف مداوم مانند پردازش صفها یا نظرسنجیهای دورهای عالی میکند.ExecuteAsync روی نخ اصلی (Main Thread) اجرا میشود تا زمانی که کنترل را با await واگذار کند. استفاده از متدهای همزمان مانند Thread.Sleep راهاندازی را بلاک میکند:public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
Thread.Sleep(500); // در .NET 9، این نخ اصلی را بلاک میکند!
}
}
}await Task.Delay(توصیهشده)await کنترل را فوراً واگذار میکند:public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
await Task.Delay(500, stoppingToken); // کنترل را واگذار میکند
}
}
}Task.Yield()public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield(); // واگذاری فوری به نخ استخر
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
Thread.Sleep(5000); // حالا ایمن است
}
}
}CancellationToken برای مدیریت توقف graceful استفاده کنید — مثلاً در سناریوهای shutdown، این توکن به شما اجازه میدهد کارهای نیمهتمام را به درستی تمام کنید.ExecuteAsync بلافاصله روی یک نخ پسزمینهی جداگانه شروع به اجرا میشود. این تغییر، نیاز به هکهای واگذاری را از بین میبرد و تجربهی توسعه را شهودیتر میکند.public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
Thread.Sleep(5000); // در .NET 10، هیچ مشکلی ندارد!
}
}
}Thread.Sleep دیگر راهاندازی را بلاک نمیکنند. با این حال، الگوهای ناهمگام همچنان برای بهرهوری بهتر منابع (مانند جلوگیری از بلاک نخها در سناریوهای پربار) توصیه میشود. این تغییر بخشی از تلاشهای مایکروسافت برای بهبود میزبان عمومی است و در ترکیب با ویژگیهای جدید مانند AOT (Ahead-of-Time Compilation) در .NET 10، عملکرد کلی برنامههای پسزمینه را افزایش میدهد.public class MyBackgroundService : BackgroundService
{
public MyBackgroundService()
{
// این DI Container را بلاک میکند
Console.WriteLine("سرویس پسزمینه در حال مقداردهی اولیه...");
Thread.Sleep(2000); // کار اولیه
Console.WriteLine("مقداردهی اولیه تمام شد.");
}
// ... پیادهسازی ExecuteAsync
}StartAsync(توصیهشده)public class MyBackgroundService : BackgroundService
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("سرویس پسزمینه در حال مقداردهی اولیه...");
await Task.Delay(2000, cancellationToken); // برای بهرهوری بهتر
Console.WriteLine("مقداردهی اولیه تمام شد.");
// این فراخوانی ExecuteAsync را شروع میکند
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"وظیفه پسزمینه در حال اجرا در: {DateTime.Now}");
Thread.Sleep(5000);
}
}
}StartAsync نه تنها بلاک را جلوگیری میکند، بلکه اجازه میدهد وابستگیهای تزریقی (مانند اتصال به دیتابیس) را با پشتیبانی از لغو مدیریت کنید. در عمل، این الگو در برنامههای واقعی مانند APIهای وب، جایی که خدمات پسزمینه با Hangfire یا Quartz ترکیب میشوند، حیاتی است.| نوع سرویس | استفاده کنید وقتی... | مزیت کلیدی |
IHostedService | نیاز به کنترل دقیق بر راهاندازی/توقف، تایمرها یا مدیریت نخ صریح دارید | جریان کنترل صریح |
BackgroundService | وظایف مداوم طولانیمدت (صفها، نظرسنجی) دارید یا boilerplate کم میخواهید | سادگی (فقط ExecuteAsync را بازنویسی کنید) |
| جنبه | داتنت 9 | داتنت 10 |
اجرایExecuteAsync | روی نخ اصلی تا اولین await | بلافاصله روی نخ جداگانه |
تأثیرThread.Sleep | بلاک راهاندازی | بدون بلاک |
| نیاز به workaround | بله (Task.Yield یا await) | خیر |
| بهترین الگو | همیشه async/await | async توصیهشده اما الزامی نیست |
ExecuteAsync، زود کنترل را واگذار کنید تا بلاک نشوید.ExecuteAsync راهاندازی را بلاک نمیکنند.BackgroundService را انتخاب کنید به دلیل سادگیاش.StartAsync را بازنویسی کنید (نه سازنده).public static class WebApplicationExtensions
{
public static async Task LoadCertificatesFromVaultOrFail(this WebApplication app)
{
// Load the certificate from the vault, with retry logic in case of failure
}
}
program.cs
var app = builder.Build();
await app.LoadCertificatesFromVaultOrFail();
app.Run();برنامه نباید بالا بیاید مگر اینکه گواهی از HashiCorp Vault (یا Key Vault، Secrets Manager و …) با موفقیت بارگذاری شده باشد و یکIHttpClientFactoryیاHttpClientسینگلتون با آن گواهی تنظیم شده باشد.
IHostApplicationLifetime + یک IHostedLifecycleService (در .NET 8+) یا سادهتر: یک BackgroundService که فقط کارش «مقداردهی اولیه بحرانی» است و تا موفق نشود، اجازه ادامه راهاندازی را نمیدهد.// 1. اول سرویس مقداردهی اولیه را تعریف میکنیم
public class CertificateInitializationService : BackgroundService
{
private readonly ILogger<CertificateInitializationService> _logger;
private readonly VaultClient _vaultClient; // یا هر کلاینتی که داری
private readonly CertificateSingleton _certificateSingleton; // سینگلتون شما
public CertificateInitializationService(
ILogger<CertificateInitializationService> logger,
VaultClient vaultClient,
CertificateSingleton certificateSingleton)
{
_logger = logger;
_vaultClient = vaultClient;
_certificateSingleton = certificateSingleton;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
const int maxRetries = 10;
var delay = TimeSpan.FromSeconds(3);
for (int i = 0; i < maxRetries; i++)
{
try
{
_logger.LogWarning("در حال بارگذاری گواهی از Vault... تلاش {Attempt}/{Max}", i + 1, maxRetries);
var certificate = await _vaultClient.GetCertificateAsync(stoppingToken);
_certificateSingleton.SetCertificate(certificate); // تنظیم سینگلتون
_logger.LogInformation("گواهی با موفقیت از Vault بارگذاری و تنظیم شد.");
return; // موفقیت → خروج از ExecuteAsync → برنامه ادامه پیدا میکند
}
catch (Exception ex) when (i < maxRetries - 1)
{
_logger.LogError(ex, "بارگذاری گواهی ناموفق بود. تلاش مجدد پس از {Delay} ثانیه...", delay);
await Task.Delay(delay, stoppingToken);
}
}
// اگر اینجا رسیدیم یعنی همه تلاشها شکست خورده
_logger.LogCritical("بارگذاری گواهی از Vault پس از {MaxRetries} تلاش ناموفق بود. برنامه متوقف میشود.", maxRetries);
// بسیار مهم: درخواست توقف برنامه
// این باعث میشود Host به طور کنترلشده shutdown شود و کد خروج غیرصفر برگردد
Environment.ExitCode = 1;
// یا اگر از IHostApplicationLifetime تزریق کرده باشی:
// _lifetime.StopApplication();
throw new InvalidOperationException("Failed to load certificate from Vault after multiple retries.");
}
}var app = builder.Build(); // اینجا برنامه تا CertificateInitializationService کامل نشود، به app.Run() نمیرسد app.Run();
BackgroundService کامل بسازید، از IStartupFilter هم میتونید استفاده کنید ولی توصیه نمیشود؛ چون قابلیت Retry و CancellationToken ندارد. بهترین گزینه همچنان BackgroundService + خروج کنترلشده است.BackgroundService اختصاصی برای مقداردهی اولیه بحرانی استفاده کن و در انتها اگر شکست خورد، Environment.Exit(1) بزن یا IHostApplicationLifetime.StopApplication() فراخوانی کن.