⏱️ ءPeriodicTimer در NET.؛ چه زمانی بهتر از Task.Delay است؟
خیلی وقتها در پروژههای NET. نیاز داریم یک عملیات را بهصورت دورهای اجرا کنیم:
🔹 هر ۳۰ ثانیه وضعیت سفارشها را بررسی کنیم
🔹 هر ۵ دقیقه Cache را Refresh کنیم
🔹 هر یک دقیقه Jobهای Pending را پردازش کنیم
🔹 هر چند دقیقه اطلاعات یک سرویس خارجی را Sync کنیم
اولین چیزی که خیلی از Developerها مینویسند چیزی شبیه این است:
while (!cancellationToken.IsCancellationRequested)
{
await DoSomethingAsync();
await Task.Delay(
TimeSpan.FromMinutes(5),
cancellationToken);
}
این کد کاملاً معتبر است و در بسیاری از سناریوها هم انتخاب خوبی است.
اما از NET 6. به بعد، یک API مشخص برای همین مدل سناریو داریم:
PeriodicTimerمایکروسافت آن را بهعنوان Timerای معرفی میکند که امکان waiting asynchronously for timer ticks را فراهم میکند.
⏱️ ءPeriodicTimer دقیقاً چیست؟
ء
PeriodicTimer در namespace زیر قرار دارد:System.Threading
و استفاده اصلی آن این است که بهجای Callback-based Timer، منتظر Tick بعدی Timer بمانیم:
using var timer = new PeriodicTimer(
TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await DoSomethingAsync(cancellationToken);
}
اینجا یک نکته مهم وجود دارد:
ء( )
WaitForNextTickAsync یک <ValueTask<bool برمیگرداند.تا زمانی که Tick بعدی اتفاق نیفتاده، کد شما منتظر میماند و بعد از Tick وارد بدنه حلقه میشود.
اگر Timer متوقف شود، این متد میتواند
false برگرداند و حلقه تمام شود. همچنین ( )Dispose میتواند یک WaitForNextTickAsync فعال را متوقف کند و باعث شود false برگردد.🔄 پس چه فرقی با Task.Delay دارد؟
مثلاً این دو کد را ببینید.
با
Task.Delay:while (!cancellationToken.IsCancellationRequested)
{
await DoSomethingAsync(cancellationToken);
await Task.Delay(
TimeSpan.FromMinutes(5),
cancellationToken);
}
و با
PeriodicTimer:using var timer = new PeriodicTimer(
TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(
cancellationToken))
{
await DoSomethingAsync(cancellationToken);
}
از نظر نتیجه ظاهری، هر دو میتوانند یک کار را بهصورت دورهای اجرا کنند.
اما مدل ذهنی آنها متفاوت است.
ء
Task.Delay میگوید:بعد از این مدت دوباره ادامه بده.
در حالی که
PeriodicTimer میگوید:من یک Timer دورهای دارم؛ وقتی Tick بعدی رسید، اجازه بده ادامه بدهم.
برای سناریوهایی که ذاتاً periodic هستند، این مدل بسیار واضحتر است.
⚠️ اما یک اشتباه مهم
نباید تصور کنیم:
PeriodicTimer
یعنی Job شما هر دقیقاً ۵ دقیقه یکبار اجرا میشود.
فرض کنید:
Period = 5 minutes
Tick #1
↓
DoSomethingAsync()
↓
7 minutes
↓
Tick #2
در این مدت شما دوباره
WaitForNextTickAsync() را صدا نزدهاید.طبق مستندات،
PeriodicTimer برای استفاده توسط یک consumer در هر لحظه طراحی شده است و فقط یک WaitForNextTickAsync() باید همزمان در حال انتظار باشد.بنابراین نباید از آن انتظار داشته باشید که برای هر Tick یک اجرای موازی از Job ایجاد کند.
🧠 این رفتار در BackgroundService خیلی کاربردی است
مثلاً فرض کنید در یک سیستم فروشگاهی میخواهیم هر ۳۰ ثانیه سفارشهای Pending را بررسی کنیم:
public sealed class OrderWorker(
IServiceScopeFactory scopeFactory)
: BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(
TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(
stoppingToken))
{
using var scope =
scopeFactory.CreateScope();
var service =
scope.ServiceProvider
.GetRequiredService<IOrderService>();
await service.ProcessPendingOrdersAsync(
stoppingToken);
}
}
}
اینجا Flow کاملاً مشخص است
و وقتی Application در حال Shutdown باشد، CancellationToken میتواند انتظار Timer را متوقف کند.
📌 پس PeriodicTimer برای چه چیزی مناسب است؟
✅ اجرای Periodic یک عملیات درون یک Process
✅ ءBackground Workerهای ساده
✅ ءPolling
✅ ءCache Refresh
✅ بررسی دورهای وضعیت
✅ ءCleanupهای ساده
✅ ءSync دورهای
و مخصوصاً زمانی که میخواهید این منطق را بهشکل async و قابل Cancellation بنویسید.