Task كائن يمثّل عملًا قيد التنفيذ. قد يعمل على خيط من مجمّع الخيوط، أو ينتظر مؤقّتًا، أو ينتظر ردًّا من الشبكة؛ وفي كل الأحوال يمكنك أن تسأل هل انتهى، وتحصل على نتيجته، وتنتظره، أو تلحق به عملًا آخر. async وawait صياغة اللغة فوق المهام. وتغطي هذه الصفحة واجهة Task نفسها، والخيوط.
Task.Run: العمل على مجمّع الخيوط
تأخذ Task.Run تعبير lambda وتضعه في طابور مجمّع الخيوط، وهو مجموعة خيوط تحتفظ بها .NET كي لا تنشئ خيطًا جديدًا لكل مهمة. وتعيد Task، أو Task<T> إن كان تعبير lambda يعيد قيمة.
المخرجات:
Started, completed yet: False
Primes up to 200,000: 17984
Status: RanToCompletion
تحجز Result الخيط المستدعي حتى تنتهي المهمة. هذا مقبول في Main برنامج كونسول، لكن داخل الشيفرة غير المتزامنة ستكتب int primes = await task;، التي تنتظر دون حجز. وينطبق الأمر نفسه على task.Wait() لـ Task بلا نتيجة.
Task.Run للعمل كثيف المعالجة: حسابات تبقي نواة مشغولة. أما للإدخال والإخراج، مثل قراءة ملف أو استدعاء واجهة API على الويب، فاستخدم الدالة غير المتزامنة التي توفرها المكتبة أصلًا (File.ReadAllTextAsync وHttpClient.GetStringAsync) وانتظرها مباشرة بـ await. فأثناء انتظار الإدخال والإخراج لا يحتجز الاستدعاء غير المتزامن المنتظَر أي خيط، لذا لا يضيف تغليفه في Task.Run شيئًا سوى مرور إضافي عبر مجمّع الخيوط. أما ما يشغل خيطًا من المجمّع فعلًا فهو Task.Run حول استدعاء متزامن حاجب مثل File.ReadAllText: يبقى ذلك الخيط خاملًا حتى يستجيب القرص أو الشبكة.
عدة مهام معًا: WhenAll
قسّم العمل المستقل إلى مهام، وابدأها كلها، وادمج النتائج بـ Task.WhenAll. على جهاز بعدة أنوية تعمل الأجزاء بالتوازي.
المخرجات:
Parts: 14999995, 14999999, 15000003, 15000000
Total: 59999997
كل جزء يعمل على نطاقه الخاص ويعيد مجموعه الخاص، فلا تتشارك المهام شيئًا ولا تحتاج إلى أقفال. وتعيد WhenAll النتائج بترتيب المهام، أيًّا كان ترتيب انتهاء الخيوط. وحين تحتاج المهام إلى تحديث حالة مشتركة راجع lock.
WhenAny والمهل الزمنية
تكتمل Task.WhenAny حين تكتمل أولى مهامها وتعيد تلك المهمة. مسابقة العمل الحقيقي ضد Task.Delay تعطي مهلة زمنية:
المخرجات:
fast mirror answered after 100 ms
Timed out after 300 ms
تعيد WhenAny المهمة الفائزة نفسها، لا نتيجتها، فتنتظرها بـ await مجددًا لتحصل على القيمة (أو الاستثناء إن فشلت). لاحظ أن المهام الخاسرة تستمر في العمل؛ لا توقفها WhenAny. ولإيقاف العمل البطيء فعلًا ألغه برمز كما يظهر أدناه. في .NET 6 وما بعده تعبّر await work.WaitAsync(TimeSpan.FromMilliseconds(300)) عن المهلة نفسها في استدعاء واحد.
الاستكمالات بـ ContinueWith
قبل await كانت طريقة تشغيل شيفرة بعد انتهاء مهمة هي ContinueWith. ما زلت ستراها في الشيفرات الأقدم:
المخرجات:
Report total: 42
يتلقى كل استكمال المهمة السابقة ويُنفَّذ حين تكتمل. تفعل await الشيء نفسه بتكلّف أقل بكثير، وتعالج الاستثناءات بشكل طبيعي، وتعود إلى السياق الصحيح، ففضّلها في الشيفرة الجديدة. وتُنفَّذ ContinueWith حتى حين تكون المهمة السابقة قد فشلت أو أُلغيت، فعلى الشيفرة الأقدم فحص t.IsFaulted أو تمرير TaskContinuationOptions لتجنّب استدعاء .Result على مهمة فاشلة.
الإلغاء بـ CancellationToken
تُلغى المهام بشكل تعاوني. يصدر CancellationTokenSource رمزًا؛ والشيفرة التي تؤدي العمل تفحص الرمز وتوقف نفسها. لا شيء يُجهض قسرًا.
المخرجات:
Cancelled before finishing all 10 orders
Processed 3
يُمرَّر الرمز عبر كل طبقة، وتقبله أيضًا واجهات API غير المتزامنة مثل Task.Delay وHttpClient واستدعاءات قواعد البيانات، فيوقفها الإلغاء في منتصف الانتظار. التقط OperationCanceledException؛ فـ TaskCanceledException التي ترميها بعض الواجهات مشتقة منها. وCancellationTokenSource من نوع IDisposable، ومن هنا using.
Thread مقابل Task
Thread خيط من نظام التشغيل تنشئه بنفسك. هو أثقل: لكل خيط مكدّسه الخاص (1 MB افتراضيًا على Windows)، ويستغرق بدؤه وقتًا ملموسًا، ولا يمنحك نتيجة ولا معالجة استثناءات.
المخرجات:
Thread started
Result: 42
Thread | Task | |
|---|---|---|
| يعمل على | خيط نظام تشغيل جديد مخصص | خيط من المجمّع (أو بلا خيط، للإدخال والإخراج) |
| كلفة البدء | عالية | منخفضة |
| القيمة الناتجة | لا، شارك متغيّرًا | Task<T>.Result / await |
| الاستثناءات | غير المعالجة تُسقط العملية | تُخزَّن في المهمة، ويُعاد رميها عند await |
| الإلغاء | علامات يدوية | CancellationToken مدمج |
| انتظاره | Join() | await وWait() وWhenAll |
لا تلجأ إلى Thread إلا حين تحتاج إلى شيء لا يمنحه المجمّع: حلقة طويلة العمر يجب ألا تستعير خيطًا من المجمّع أبدًا، أو أولوية معينة، أو شقة أحادية الخيط لـ COM. وللمهام طويلة العمر تحصل Task.Factory.StartNew(work, TaskCreationOptions.LongRunning) على خيط مخصص مع الإبقاء على واجهة المهام.
Parallel.For وParallel.ForEach
لـ "افعل هذا بكل عنصر، مستخدمًا كل الأنوية" تكون System.Threading.Tasks.Parallel أبسط من إنشاء المهام يدويًا. تقسّم النطاق على الخيوط وتعود حين تنتهي كل التكرارات:
مخرجات على سبيل المثال:
image 4 on thread 7
image 3 on thread 6
image 5 on thread 8
image 0 on thread 1
image 1 on thread 4
image 2 on thread 5
Total pixels: 385425
تخرج أسطر كل صورة بأي ترتيب نفّذتها به الخيوط، وتختلف معرّفات الخيوط من تشغيل لآخر؛ المجموع وحده ثابت. هذه طبيعة الحلقات المتوازية: يجب ألا تعتمد التكرارات على ترتيب بعضها، وأي حالة مشتركة تحتاج إلى تزامن (Interlocked.Add هنا). وتفعل Parallel.ForEach الشيء نفسه على أي مجموعة، وتضيف .NET 6 الدالة Parallel.ForEachAsync للأجسام غير المتزامنة.
أخطاء شائعة
.Resultأو.Wait()داخل شيفرة غير متزامنة. تحجز خيطًا وقد تسبّب الجمود في تطبيقات الواجهات؛ استخدمawaitبدلًا منهما.- التقاط نوع الاستثناء الأصلي حول
.Result. تصل الإخفاقات مغلّفة فيAggregateException؛ وawaitتفكّ تغليفها. - أطلق وانسَ.
Task.Run(...)دون انتظار المهمة أو تخزينها يضيّع استثناءاتها. Task.Runحول الإدخال والإخراج. حول دالة غير متزامنة لا يضيف سوى مرور إضافي عبر مجمّع الخيوط، وحول دالة حاجبة مثلFile.ReadAllTextيحتجز خيطًا من المجمّع طوال مدة الانتظار. انتظر دالة الإدخال والإخراج غير المتزامنة مباشرة.- تجاهل الرمز. تمرير
CancellationTokenلا يفعل شيئًا ما لم يفحصه العمل. - حالة مشتركة غير متزامنة في الشيفرة المتوازية. استخدم
Interlockedأوlock، أو امنح كل مهمة بياناتها الخاصة.
الأسئلة الشائعة
ما هي Task في C#؟
تمثّل Task عملية قد تنتهي في المستقبل: تشغيل شيفرة على مجمّع الخيوط، أو مؤقّت، أو طلب إدخال وإخراج. يمكن انتظارها، وتبلّغ هل اكتملت أو فشلت أو أُلغيت، وTask<T> تحمل أيضًا نتيجة. المهام هي اللبنة التي تقوم عليها async/await.
ما الفرق بين Task وThread في C#؟
Thread خيط من نظام التشغيل تنشئه وتديره بنفسك، بمكدّس خاص به؛ بدؤه مكلف ولا يعيد قيمة. أما Task فتصف عملًا، وتجدوله Task.Run على مجمّع مشترك من الخيوط القابلة لإعادة الاستخدام؛ يمكنها إعادة نتيجة، ونشر الاستثناءات، والإلغاء، والانتظار. استخدم المهام ما لم تحتج إلى خيط مخصص طويل العمر بإعدادات خاصة.
متى أستخدم Task.Run؟
للعمل كثيف المعالجة الذي تريده بعيدًا عن الخيط الحالي: معالجة الصور، أو حساب كبير، أو تحليل ملف كبير في الذاكرة، خاصة لإبقاء الواجهة مستجيبة. لا تغلّف الإدخال والإخراج في Task.Run: دوال الإدخال والإخراج غير المتزامنة مثل ReadAllTextAsync تحرّر الخيط أصلًا أثناء الانتظار.
كيف ألغي Task في C#؟
أنشئ CancellationTokenSource، ومرّر Token الخاص به إلى العمل، واستدعِ Cancel() (أو CancelAfter(timeout)) على المصدر. يجب أن يتعاون العمل: افحص token.IsCancellationRequested أو استدعِ token.ThrowIfCancellationRequested() في حلقته، ومرّر الرمز إلى واجهات API غير المتزامنة مثل Task.Delay. انتظار مهمة ملغاة يرمي OperationCanceledException.
ما الفرق بين Task.WhenAll وTask.WhenAny؟
تكتمل Task.WhenAll حين تكتمل كل مهمة وتعطيك كل النتائج. وتكتمل Task.WhenAny بمجرد اكتمال أول مهمة وتعيد تلك المهمة. وWhenAny هي الطريقة المعتادة لإضافة مهلة زمنية: سابِق العمل ضد Task.Delay.
لماذا ترمي Task.Result استثناء AggregateException؟
تغلّف .Result و.Wait() أي فشل في AggregateException، لأن المهمة قد تحمل عمومًا عدة استثناءات. أما await فتفكّ التغليف وتعيد رمي أول استثناء داخلي مباشرة، وهذا أحد أسباب تفضيل await. وإن كان لا بد من الحجز فإن .GetAwaiter().GetResult() ترمي أيضًا الاستثناء الأصلي.