المهام الدورية والمؤقتات
جدوِلوا الأعمال المتكررة باستخدام PeriodicTimer وSystem.Threading.Timer، وأديروا الإلغاء بسلاسة.
المهام الدورية والمؤقتات درس مجاني في C# Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في C# Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة C# Academy 4 دروس في المجموع.
جدولة العمل المتكرر
تحتاج العديد من الخدمات الخلفية إلى التشغيل وفق جدول ثابت، كل 5 دقائق أو كل ساعة أو يوميًا. يوفّر .NET عدة آليات لذلك: PeriodicTimer (حديثة)، وSystem.Threading.Timer (تقليدية)، وQuartz.NET (تعتمد على cron).
PeriodicTimer: الخيار الحديث
صُممت PeriodicTimer (التي قُدّمت في .NET 6) للاستخدام في الأساليب async. وهي تعمل وفق الجدول بدقة من دون استدعاءات callback، وتتكامل بسلاسة مع الإلغاء.
public class MetricsCollectorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
{
// Runs every 60 seconds
await CollectMetricsAsync(ct);
}
// Loop exits cleanly when ct is cancelled
}
}PeriodicTimer مقابل حلقة Task.Delay
تُعد PeriodicTimer أفضل من حلقة Task.Delay؛ إذ تعمل على فواصل زمنية ثابتة بدءًا من وقت البدء (وليس وقت البدء + وقت المعالجة)، مما يمنع الانجراف الزمني عبر التكرارات الكثيرة.
// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
await DoWorkAsync(ct); // takes 2 seconds
await Task.Delay(5000, ct); // actual period: 7 seconds
}
// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
await DoWorkAsync(ct); // period is still 5 seconds from start
}System.Threading.Timer
System.Threading.Timer مؤقّت تقليدي لمجمّع مؤشرات الترابط، ويستخدم callback. وهو يدعم وضعي التشغيل الدوري والتشغيل لمرة واحدة. انتبه إلى أن callback يعمل على مؤشر ترابط من مجمّع مؤشرات الترابط من دون دعم async.
public class LegacyTimerService : BackgroundService, IDisposable
{
private Timer? _timer;
protected override Task ExecuteAsync(CancellationToken ct)
{
// Fire immediately then every 10 seconds
_timer = new Timer(DoWork, null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
// Runs on thread pool — avoid async void here
Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
}
public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}التنفيذ المؤجل لمرة واحدة
استخدم PeriodicTimer مع نبضة واحدة، أو ادمج CancellationTokenSource.CancelAfter مع Task.Delay لتنفيذ مؤجل لمرة واحدة.
// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
await Task.Delay(delay, ct);
await action();
}
// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
await RunScheduledJobAsync();
}PeriodicTimer مع معالجة الأخطاء
غلّف دائمًا العمل الخاص بكل نبضة في try/catch حتى لا يؤدي فشل واحد إلى إيقاف المؤقّت عن إصدار النبضات في الدورة التالية.
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
await SyncInventoryAsync(ct);
}
catch (Exception ex)
{
// Log but don't crash — next tick will retry
_logger.LogError(ex, "Inventory sync failed");
}
}
}المحاذاة مع حدود الساعة
للتشغيل في أوقات محددة من الساعة، مثل كل ساعة عند بدايتها، احسب التأخير الأولي حتى الحد الزمني التالي قبل بدء المؤقّت.
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Wait until the next full hour
var now = DateTime.UtcNow;
var nextHour = now.Date.AddHours(now.Hour + 1);
var initialDelay = nextHour - now;
await Task.Delay(initialDelay, ct);
// Then tick every hour exactly
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
await RunJobAsync(ct); // run at the first tick
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}الحماية من تزامن النبضات
إذا كان تنفيذ العمل قد يستغرق وقتًا أطول من فترة المؤقّت، فاحمِ من التنفيذ المتزامن باستخدام SemaphoreSlim، أو تخطَّ النبضة إذا كان التنفيذ لا يزال جاريًا.
private readonly SemaphoreSlim _lock = new(1, 1);
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(ct))
{
// Skip if previous tick is still running
if (!await _lock.WaitAsync(0, ct))
{
_logger.LogWarning("Skipping tick — previous still running");
continue;
}
try { await DoWorkAsync(ct); }
finally { _lock.Release(); }
}
}إضافة التذبذب إلى الخدمات الموزعة
عند بدء مثيلات متعددة من الخدمة في الوقت نفسه، تمنع إضافة تذبذب عشوائي إلى التأخير الأولي حدوث مشكلة القطيع المتدافع على الموارد المشتركة.
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Add jitter: 0-5 seconds random delay on startup
var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
await Task.Delay(jitter, ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}تطبيق واقعي: خدمة تحديث ذاكرة التخزين المؤقت
خدمة دورية تحدّث ذاكرة تخزين مؤقت موزعة كل 10 دقائق، مع إضافة تذبذب واسترداد من الأخطاء.
public class CacheRefreshService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<CacheRefreshService> _log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Spread startups across instances
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
using var scope = _factory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
await cache.RefreshAllAsync(ct);
_log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
}
catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
}
}
}تحقق سريع
لماذا يُفضّل استخدام PeriodicTimer على حلقة Task.Delay للمهام المتكررة؟
مراجعة: المهام الدورية والمؤقتات
أهم النقاط:
-
PeriodicTimer: async، يراعي الإلغاء، ولا يسبب انجرافًا زمنيًا — الخيار الحديث -
System.Threading.Timer: يعتمد على callback، ومناسب للتكامل مع الشيفرة القديمة - غلّف العمل الخاص بكل نبضة في try/catch حتى لا توقف الأخطاء المؤقّت
- أضف تذبذبًا إلى المثيلات الموزعة لتجنب مشكلة القطيع المتدافع
- استخدم
SemaphoreSlimلتخطي النبضة إذا كان التنفيذ السابق لا يزال جاريًا - احسب التأخير الأولي لمحاذاة النبضات مع حدود الساعة
الأسئلة الشائعة
هل درس «المهام الدورية والمؤقتات» مجاني؟
نعم — نص درس «المهام الدورية والمؤقتات» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة C# Academy، انتقل إلى CoddyKit PRO. تتضمن دورة C# Academy 4 دروس في المجموع.
ماذا ستتعلم في «المهام الدورية والمؤقتات»؟
جدوِلوا الأعمال المتكررة باستخدام PeriodicTimer وSystem.Threading.Timer، وأديروا الإلغاء بسلاسة. تتمرن على C# Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ C# Academy؟
لا تُشترط خبرة سابقة. C# Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.
كم من الوقت يستغرق درس «المهام الدورية والمؤقتات»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس C# Academy هذا؟
نعم. كل درس في C# Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- IHostedService وBackgroundService
- مشاريع Worker Service
- المهام الدورية والمؤقتات
- المهام المجدولة في Quartz.NET