يشغّل المؤقّت (timer) شيفرة بعد تأخير أو على فاصل متكرر: تحديث ذاكرة مؤقتة كل دقيقة، أو حفظ مسودة كل 30 ثانية، أو استطلاع خدمة، أو تحديث ساعة. في .NET عدة أصناف مؤقّتات تبدو متشابهة لكنها تختلف في مكان تنفيذ دالة الاستدعاء الراجع وفي طريقة التحكم فيها. تغطي هذه الصفحة كلًّا منها، إضافة إلى Stopwatch لقياس المدة التي استغرقها شيء ما.
أي مؤقّت تستخدم
| المؤقّت | أين تُنفَّذ دالة الاستدعاء الراجع | الأسلوب | الاستخدام |
|---|---|---|---|
System.Threading.Timer | مجمّع الخيوط | مفوّض استدعاء راجع | العمل في الخلفية في الخدمات والمكتبات |
System.Timers.Timer | مجمّع الخيوط (افتراضيًا) | الحدث Elapsed، وStart/Stop | الشيفرة بأسلوب المكوّنات التي تحب الأحداث |
PeriodicTimer (.NET 6+) | حيث تعمل حلقتك غير المتزامنة | await WaitForNextTickAsync() | الشيفرة غير المتزامنة؛ لا تتداخل النبضات أبدًا |
System.Windows.Forms.Timer، وDispatcherTimer في WPF | خيط الواجهة | الحدث Tick | تحديث عناصر التحكم في تطبيقات سطح المكتب |
يُطلق الأوّلان على خيوط مجمّع الخيوط، فيمكن أن تعمل استدعاءاتهما في الوقت نفسه مع بقية برنامجك، وحتى في الوقت نفسه مع بعضها. أما مؤقّتات الواجهة فتعمل على خيط الواجهة، ولهذا تستطيع لمس عناصر التحكم مباشرة، ولهذا يجمّد المعالج البطيء النافذة.
System.Threading.Timer
يأخذ المُنشئ دالة الاستدعاء الراجع، وكائن حالة، والتأخير قبل النبضة الأولى، والدورة. ويبدأ فورًا.
مخرجات على سبيل المثال:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
تفصيلان مهمان هنا. يجب أن تنتظر Main (done.WaitOne())؛ فبرنامج الكونسول ينتهي حين تعود Main، واستدعاءات المؤقّت تعمل على خيوط خلفية لا تبقي العملية حية. والمؤقّت يعيش في حقل ساكن: System.Threading.Timer الذي لا يشير إليه شيء يمكن جمعه كنفايات، وعندها يتوقف عن الإطلاق بصمت. وهذا يلدغ أكثر ما يلدغ حين يُنشأ مؤقّت كمتغيّر محلي داخل دالة تعود.
تعيد Change(dueTime, period) جدولة المؤقّت؛ وTimeout.Infinite للاثنين توقفه مؤقتًا، وزوج جديد يعيد تشغيله. وتوقفه Dispose() نهائيًا.
System.Timers.Timer
يغلّف System.Timers.Timer الآلية نفسها في واجهة قائمة على الأحداث: اضبط Interval، واشترك في Elapsed، ثم Start() وStop().
مخرجات على سبيل المثال:
autosave #1
autosave #2
autosave #3
autosave stopped
تجعل AutoReset = false المؤقّت لمرة واحدة: يُطلق مرة ويتوقف، واستدعاء Start() مجددًا يجدول النبضة المفردة التالية. وEnabled = true وfalse مثل Start() وStop(). ويُكتب الاسم الكامل System.Timers.Timer هنا لأن في System.Threading أيضًا Timer؛ ومع استيراد فضاءي الأسماء يكون Timer المجرد ملتبسًا ولا يُترجم.
لـ System.Timers.Timer أيضًا الخاصية SynchronizingObject التي تنقل Elapsed إلى خيط الواجهة في WinForms. عمليًا يكون مؤقّت إطار الواجهة نفسه أبسط لذلك.
الاستدعاءات تتداخل وتعمل على خيوط أخرى
يُطلق الصنفان أعلاه على مجمّع الخيوط، في موعدهما، سواء انتهت دالة الاستدعاء السابقة أم لا. إن استغرقت دالة الاستدعاء 3 ثوانٍ وكانت الدورة ثانية واحدة، تعمل ثلاث استدعاءات معًا. وهذا يفرض التزامين:
- كل ما تلمسه دالة الاستدعاء الراجع يجب أن يكون آمنًا مع الخيوط. استخدم
Interlockedللعدّادات (كما تفعل الأمثلة) وlock لأي شيء أكبر. - احمِ من التداخل حين يهمّ ذلك. إما أن تتخطى نبضة ما دامت السابقة تعمل، أو تستخدم نمطًا لا يمكن أن يتداخل.
حارس تخطٍّ بـ Interlocked:
private int running = 0;
private void OnTick(object state)
{
if (Interlocked.Exchange(ref running, 1) == 1) return; // previous tick still busy
try
{
SyncOrders(); // slow work
}
finally
{
Volatile.Write(ref running, 0);
}
}
الاستثناءات هي الفخ الآخر. الاستثناء الذي يفلت من دالة استدعاء System.Threading.Timer يُسقط العملية، بينما يبتلع System.Timers.Timer الاستثناءات من معالجات Elapsed فيمرّ الفشل دون ملاحظة. غلّف جسم دالة استدعاء المؤقّت في try/catch وسجّل.
PeriodicTimer والحلقات غير المتزامنة
في الشيفرة غير المتزامنة تكون الحلقة التي تنتظر بين التكرارات أوضح مؤقّت: لا يبدأ الانتظار التالي إلا بعد انتهاء العمل، فلا يمكن أن تتداخل النبضات أبدًا، وتظهر الاستثناءات حيث تستطيع التقاطها. أضافت .NET 6 النوع PeriodicTimer لهذا بالضبط:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
تعيد WaitForNextTickAsync القيمة false بمجرد التخلّص من المؤقّت، فتنتهي الحلقة، وترمي OperationCanceledException حين يُلغى الرمز. وبخلاف حلقة حول Task.Delay يحافظ PeriodicTimer على إيقاع ثابت: إن استغرق العمل ثانيتين تأتي النبضة التالية مع ذلك بعد 30 ثانية من السابقة، لا 32.
وفي أي إصدار يعمل الشكل نفسه مع Task.Delay حين لا يهمّ الانجراف:
مخرجات على سبيل المثال:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
يحلّ الإلغاء محل Stop(): مرّر رمزًا إلى الحلقة وألغه من الخارج. راجع المهام للمزيد عن رموز الإلغاء.
قياس الوقت المنقضي بـ Stopwatch
يجدول المؤقّت الشيفرة؛ ويقيس Stopwatch المدة التي استغرقتها الشيفرة. يقرأ ساعة عالية الدقة لا تتحرك إلا للأمام، وهذا يجعله الأداة الصحيحة للتوقيت:
مخرجات على سبيل المثال:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
تتحكم فيه Start وStop وReset وRestart؛ وElapsed من نوع TimeSpan، وتعطي ElapsedMilliseconds وElapsedTicks أرقامًا خامًا. تجنّب التوقيت بـ DateTime.Now: قد تكون دقته خشنة حتى 10 إلى 15 ms، ويقفز حين تُعدَّل ساعة النظام. وللمقاييس المعيارية الجادة، حيث يشوّه إحماء JIT وجمع النفايات تشغيلًا واحدًا، استخدم مكتبة BenchmarkDotNet.
دقة المؤقّت
فاصل المؤقّت حد أدنى لا وعد. على Windows تبلغ دقة مؤقّت النظام الافتراضية نحو 15.6 ms، فيميل فاصل 10 ms إلى الإطلاق كل 15 أو 16 ms، وعلى جهاز مشغول قد تبدأ دالة استدعاء مجمّع الخيوط أبعد من ذلك. المؤقّتات مناسبة لـ "نحو كل N ثانية". أما لتوقيت الإطارات الدقيق أو الوسائط فاستخدم واجهات API مبنية لذلك.
أخطاء شائعة
System.Threading.Timerمخزّن في متغيّر محلي فقط. قد يُجمع كنفايات ويتوقف عن الإطلاق؛ احتفظ به في حقل.- ترك
Mainتعود. تنتهي العملية وتأخذ المؤقّت معها. Timerالملتبس. مع استيرادSystem.ThreadingوSystem.Timersكليهما اكتب الاسم الكامل.- افتراض أن النبضات لا تتداخل أبدًا. مؤقّتات مجمّع الخيوط تتداخل حين يكون العمل أبطأ من الفاصل.
- لمس عناصر تحكم الواجهة من مؤقّت مجمّع الخيوط. استخدم مؤقّت إطار الواجهة أو انقل التنفيذ إلى خيط الواجهة.
- الاستثناءات غير المعالجة في الاستدعاءات. تُسقط العملية (
Threading.Timer) أو تختفي (Timers.Timer)؛ التقطها وسجّلها. - نسيان
Dispose. تمسك المؤقّتات موارد النظام وتستمر في الإطلاق حتى يُتخلّص منها.
الأسئلة الشائعة
أي مؤقّت أستخدم في C#؟
في الشيفرة غير المتزامنة على .NET 6 أو أحدث، PeriodicTimer مع await timer.WaitForNextTickAsync() في حلقة. ولدالة استدعاء راجع على مجمّع الخيوط، System.Threading.Timer. ولمؤقّت قائم على الأحداث بـ Start وStop وحدث Elapsed، System.Timers.Timer. وفي WinForms أو WPF استخدم مؤقّت إطار الواجهة كي يُنفَّذ المعالج على خيط الواجهة.
كيف أشغّل شيفرة كل بضع ثوانٍ في C#؟
أنشئ مؤقّتًا بالفاصل، مثل new System.Threading.Timer(_ => Refresh(), null, 0, 5000) لاستدعاء Refresh الآن وكل 5 ثوانٍ، واحتفظ بمرجع إليه. وفي الشيفرة غير المتزامنة دُر بـ PeriodicTimer (.NET 6+) أو await Task.Delay(5000)؛ ونسخة الحلقة لا تشغّل نبضتين معًا أبدًا.
كيف أوقف مؤقّتًا في C#؟
في System.Timers.Timer استدعِ Stop() (أو اضبط Enabled = false)، وDispose() حين لا تعود تحتاجه. وفي System.Threading.Timer استدعِ Change(Timeout.Infinite, Timeout.Infinite) لإيقافه مؤقتًا، أو Dispose() لإيقافه نهائيًا. ودالة الاستدعاء الراجع التي بدأت أصلًا قد تنتهي بعد أن توقف المؤقّت.
لماذا يتوقف System.Threading.Timer عن الإطلاق؟
لا شيء يشير إلى المؤقّت، فجمعه جامع النفايات وتوقفت استدعاءاته. يحدث هذا حين يُنشأ المؤقّت كمتغيّر محلي في دالة تعود. خزّنه في حقل ما دام يجب أن يستمر في العمل.
كيف أقيس الوقت المنقضي في C#؟
استخدم System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); ثم اقرأ sw.ElapsedMilliseconds أو sw.Elapsed. يستخدم ساعة رتيبة عالية الدقة، بخلاف طرح قيمتي DateTime.Now، الذي دقته أخشن ويقفز حين تُعدَّل ساعة النظام.