C# / المتوسط: التركيب والعمل غير المتزامن / الدرس 9
اللاتزامن والانتظار وأعمار المهام
اشرح التعليق والاكتمال بدقة ولاحظ العمل دون افتراض خيط جديد لكل مهمة.
في هذه الصفحة
أهداف الدرس
- تمييز الانتظار غير المتزامن من التوازي.
- ملاحظة نتائج المهام وفشلها بـawait.
- تجنب الحجب وعمل async void بلا مالك.
async تحويل تدفق تحكم
تبدأ الطريقة async بالتنفيذ المتزامن حتى تبلغ await تحتاج فعلا إلى التعليق. إذا كانت العملية المنتظرة مكتملة فقد تستمر الطريقة دون تعليق. تمثل Task اكتمال عملية، ولا تعني بالضرورة تخصيص خيط لها. لا يحتاج انتظار الإدخال والإخراج غالبا إلى خيط محجوب طوال المدة، بينما يحتاج الحساب إلى موارد تنفيذ فعلية.
يحول المترجم الطريقة إلى آلة حالات تستأنف العمل بعد التعليق. لا تعد الشيفرة بعد await بالعمل على خيط محدد في كل البيئات؛ إذ يؤثر SynchronizationContext وجدول المهام في موضع الاستمرار. لذلك لا يجوز تعميم سلوك مثال الطرفية بوصفه ضمانا لخيط واجهة المستخدم.
انتظار عملية مكتملة بالفعل
يعيد المثال الأول Task.FromResult ثم ينتظرها. يوضح ذلك عقد النتيجة دون الادعاء بمحاكاة تأخير شبكة أو إنشاء خيط جديد. تكون القيمة متاحة فورا، لكن المستدعي يستخدم صيغة await نفسها التي تصلح لعملية غير متزامنة لم تكتمل بعد.
لا تضف async لمجرد تغليف قيمة جاهزة إذا كان إرجاع المهمة المكتملة مباشرة أوضح. وفي المقابل قد تحتاج طريقة تنفذ تنظيفا أو ترجمة للاستثناء حول await فعلية إلى آلة الحالات حتى يصح عمر الموارد. اختر بناء على الدلالة، لا قاعدة عامة تفضل حذف async أو إضافتها دائما.
using System;
using System.Threading.Tasks;
int value = await ReadyAsync();
Console.WriteLine($"value={value}");
static Task<int> ReadyAsync() => Task.FromResult(42);
value=42
تجلب await اثنين وأربعين دون ادعاء إنشاء خيط.
بدء العمل المرتبط وملاحظة الجميع
تمثل Task.WhenAll اكتمال جميع المهام المقدمة إليها. وهي لا تبدأ تلك المهام بنفسها، ولا تحد التزامن، ولا تلغي المهام الأخرى تلقائيا عند فشل واحدة. احتفظ بالمهام حتى ينتظر المالك كل العمل ويفحص الأخطاء وفق سياسته. قد يتجاوز الاستدعاء المنسي عمر موارده ويفقد سياق فشله.
يبدأ المثال الثاني حسابين اصطناعيين محدودين يتنازل كل منهما مرة واحدة، ثم ينتظر كليهما. قد يختلف ترتيب الجدولة، لكن جمع النتيجتين يعطي ستة دائما. لا يستخدم المثال نوما ولا خدمة خارجية. وعند معالجة عناصر كثيرة يجب تحديد قبول العمل بدلا من إطلاق مهمة بلا حد لكل سجل غير موثوق.
using System;
using System.Linq;
using System.Threading.Tasks;
Task<int> first = DoubleAsync(1);
Task<int> second = DoubleAsync(2);
int[] values = await Task.WhenAll(first, second);
Console.WriteLine($"sum={values.Sum()}");
static async Task<int> DoubleAsync(int value)
{
await Task.Yield();
return value * 2;
}
sum=6
تلاحظ WhenAll النتيجتين ويجمع المضاعفان الثابتان لستة مهماالجدولة.
تجنب الحجب المتزامن لعمل غير متزامن
تحجب .Result وWait الخيط المستدعي. وفي سياق مزامنة ذي قدرة تنفيذ مقيدة قد ينشأ جمود عندما يحتاج الاستمرار إلى السياق نفسه الذي يحجبه المستدعي. وحتى عندما لا تقع هذه الدورة، قد يهدر الحجب الخيوط أو يساهم في الجوع. فضل تمرير await عبر سلسلة الاستدعاءات عندما تسمح الواجهة.
قد تمنع ConfigureAwait(false) التقاط السياق لذلك الانتظار، لكنها ليست علاجا عاما لكل جمود ولا تفرض إنشاء خيط جديد. يجب أن يحافظ كود التطبيق الذي يحتاج سياق واجهة المستخدم على متطلباته، وأن يختار كود المكتبة سياسة الاستمرار عمدا. لا تجعل Task.Run غلافا افتراضيا لإخفاء الحجب حول إدخال وإخراج غير متزامنين.
طرق تعيد مهام لاكتمال ملحوظ
تلاحظ await فشل المهمة باستثناء عند حد الانتظار. وقد تحتوي مهمة WhenAll على عدة إخفاقات رغم أن await تعيد رمي استثناء واحد؛ لذا احتفظ بالمهمة الجامعة إذا احتاجت السياسة إلى فحص الجميع. يجب أن يحيط التنظيف بالعمر الذي ينتظر الاكتمال، لا بمجرد بدء المهمة.
تستخدم async void عادة لتواقيع معالجات الأحداث التي تتطلبها، لا للعمليات العادية القابلة لإعادة الاستخدام. لا يستطيع مستدعيها انتظار الاكتمال عبر مهمة معادة أو ملاحظة الفشل بالطريقة نفسها. اختبر الفشل قبل التعليق وبعده وفشل عدة مهام وتنظيف الموارد. اللاحقة Async اصطلاح تسمية وليست بروتوكول ملكية.
الأخطاء الشائعة وإصلاحها
تبدأ مهمة داخل using ثم تعاد بعد التخلص من المورد.
- لماذا يحدث؟
- يغطي النطاق بدء العملية بدلا من اكتمالها.
- كيف تصلحه؟
- انتظر العملية ضمن عمر المورد أو انقل الملكية صراحة.
- كيف تتجنب تكراره؟
- اختبر الوصول بعد التعليق والتنظيف عند الفشل.
تتجمد واجهة المستخدم أثناء انتظار .Result.
- لماذا يحدث؟
- يتعارض الحجب المتزامن مع استمرار يحتاج السياق نفسه.
- كيف تصلحه؟
- مرر await وراجع متطلبات سياق المزامنة.
- كيف تتجنب تكراره؟
- أبق مقطع الحجب المقصود غير قابل للتشغيل واختبر الإصلاح غير المتزامن.
لا يصل الفشل الخلفي إلى المستدعي.
- لماذا يحدث؟
- تستخدم العملية async void أو يتجاهل المستدعي المهمة.
- كيف تصلحه؟
- أعد مهمة ولاحظها ضمن عملية تملكها.
- كيف تتجنب تكراره؟
- حدد مسؤولية الاكتمال والفشل لكل عمل مطلق.
اختبر فهمك
1. هل تحد Task.WhenAll تلقائيا دفعة من عشرة آلاف مهمة بعدد صغير من العاملين؟ اشرح تصميما أكثر ضبطا.
اعرض الحل المشروح
لا؛ تجمع WhenAll اكتمال المهام المقدمة ولا تضبط قبول العمل. استخدم تصميما محدود العاملين أو قائما على سمافور، ومرر الإلغاء ولاحظ جميع المهام التي بدأت. حد الصف المنتظر أيضا، لا النشاط وحده. لا يبرر نجاح مثال صغير إطلاق مهمة لكل عنصر في مدخل معاد بلا حدود.
2. لماذا قد تستمر await لمهمة مكتملة دون تنازل، وما الافتراض الذي ينبغي للاختبار تجنبه؟
اعرض الحل المشروح
تفحص await حالة العملية ويمكنها جلب النتيجة تزامنيا إذا كانت مكتملة. ينبغي للاختبار التحقق من النتائج والترتيب المطلوب، لا افتراض انتقال كل طريقة async إلى خيط آخر أو توقفها دائما. إذا احتاج الاختبار التحكم في عملية غير مكتملة فعلا، فقدم آلية صريحة لإكمال المهمة بدلا من الاعتماد على تأخير عشوائي.
أهم ما تعلمته
- تمثل المهمة اكتمال عملية، لا خيطا مخصصا.
- انتظر العمر الذي تملكه ولاحظ كل عملية بدأت.
- تركب WhenAll المهام ولا توفر الإلغاء وحدود التزامن تلقائيا.