باختصار: Async / Await أفضل الممارسات في .NET

تحسبًا لبدء الدورة ، أعدت "C # Developer" ترجمة لمواد مثيرة للاهتمام.




غير متزامن / انتظار - مقدمة


تم إنشاء لغة Async / Await منذ الإصدار 5.0 من C # (2012) وأصبحت بسرعة أحد ركائز برمجة .NET الحديثة - يجب على أي مطور C # يحترم نفسه أن يستخدمه لتحسين أداء التطبيق والاستجابة العامة وإمكانية قراءة التعليمات البرمجية.

يجعل Async / Await إدخال التعليمات البرمجية غير المتزامنة أمرًا بسيطًا بشكل خادع ويزيل الحاجة إلى المبرمج لفهم تفاصيل معالجته ، ولكن كم منا يعرف حقًا كيف يعمل وما هي مزايا وعيوب هذه الطريقة؟ هناك الكثير من المعلومات المفيدة ، لكنها مجزأة ، لذلك قررت كتابة هذه المقالة.

حسنًا ، دعنا نتعمق في الموضوع.

آلة الدولة (IAsyncStateMachine)


أول شيء تحتاج إلى معرفته هو أنه تحت غطاء المحرك في كل مرة يكون لديك طريقة أو وظيفة مع Async / Await ، يقوم المترجم بالفعل بتحويل طريقتك إلى فصل دراسي يطبق واجهة IAsyncStateMachine. هذه الفئة مسؤولة عن الحفاظ على حالة طريقتك خلال دورة حياة عملية غير متزامنة - فهي تغلف جميع متغيرات طريقتك في شكل حقول وتقسيم الكود الخاص بك إلى أقسام يتم تنفيذها أثناء عمليات انتقال آلة الحالة بين الحالات ، بحيث يمكن لمؤشر الترابط مغادرة الطريقة ومتى سيعود ، لن تتغير الدولة.

كمثال ، هنا تعريف فئة بسيط للغاية مع طريقتين غير متزامنتين:

استخدام System.Threading.Tasks ؛

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

فئة ذات طريقتين غير متزامنتين

إذا نظرنا إلى التعليمات البرمجية التي تم إنشاؤها أثناء التجميع ، فسوف نرى شيئًا مثل هذا:



لاحظ أن لدينا فئتين داخليتين جديدتين تم إنشاؤهما لنا ، واحدة لكل طريقة غير متزامنة. تحتوي هذه الفئات على جهاز حالة لكل من طرقنا غير المتزامنة.

علاوة على ذلك ، بعد دراسة الشفرة المترجمة <AsyncAwaitExample> d__0، سنلاحظ أن المتغير الداخلي لدينا «myVariable»أصبح الآن حقل فئة:



يمكننا أيضًا رؤية حقول فئة أخرى مستخدمة داخليًا للحفاظ على الحالة IAsyncStateMachine. آلة الحالة تمر عبر الحالات باستخدام الطريقةMoveNext()، في الواقع ، مفتاح كبير. لاحظ كيف تستمر الطريقة في أقسام مختلفة بعد كل من المكالمات غير المتزامنة (مع ملصق المتابعة السابق).



وهذا يعني أن الأناقة غير المتزامنة / تنتظر تأتي بثمن. يضيف استخدام المزامنة / الانتظار بعض التعقيد (والذي قد لا تكون على دراية به). في المنطق من جانب الخادم ، قد لا يكون هذا حرجًا ، ولكن على وجه الخصوص عند برمجة تطبيقات الهاتف المحمول التي تأخذ في الاعتبار كل دورة ذاكرة CPU و KB ، يجب أن تضع ذلك في الاعتبار ، حيث يمكن أن تزيد كمية النفقات العامة بسرعة. لاحقًا في هذه المقالة ، سنناقش أفضل الممارسات لاستخدام Async / Await فقط عند الضرورة.

للحصول على شرح مفيد جدًا لجهاز الدولة ، شاهد هذا الفيديو على YouTube.

متى تستخدم Async / Await


يوجد بشكل عام سيناريوهان حيث Async / Await هو الحل الصحيح.

  • العمل المرتبط بإدخال / إخراج : يتوقع الرمز الخاص بك شيئًا ، مثل البيانات من قاعدة بيانات ، أو قراءة ملف ، أو الاتصال بخدمة ويب. في هذه الحالة ، يجب عليك استخدام Async / Await ، وليس مكتبة المهام المتوازية.
  • العمل المتعلق بوحدة المعالجة المركزية : سيقوم الكود الخاص بك بإجراء حسابات معقدة. في هذه الحالة ، يجب عليك استخدام Async / Await ، لكنك تحتاج إلى بدء العمل في موضوع آخر باستخدام Task.Run. يمكنك أيضًا التفكير في استخدام Task Parallel Library .



غير متزامن على طول الطريق


عندما تبدأ في العمل بأساليب غير متزامنة ، ستلاحظ بسرعة أن الطبيعة غير المتزامنة للشفرة تبدأ في الانتشار والتدرج في التسلسل الهرمي للمكالمات - وهذا يعني أنه يجب عليك أيضًا جعل رمز الاتصال الخاص بك غير متزامن وما إلى ذلك.
قد تميل إلى "إيقاف" هذا من خلال حظر التعليمات البرمجية باستخدام Task.Result أو Task.Wait ، تحويل جزء صغير من التطبيق ولفه في API متزامن بحيث يتم عزل بقية التطبيق من التغييرات. لسوء الحظ ، هذه وصفة لخلق طريق مسدود يصعب تتبعه.

أفضل حل لهذه المشكلة هو السماح للتعليمات البرمجية غير المتزامنة بالنمو في قاعدة التعليمات البرمجية بشكل طبيعي. إذا اتبعت هذا القرار ، فسترى امتداد الكود غير المتزامن إلى نقطة دخوله ، وعادة ما يكون معالج حدث أو إجراء تحكم. الاستسلام للتزامن دون أن يترك أثرا!

مزيد من المعلومات في هذه المقالة MSDN.

إذا تم إعلان الطريقة غير متزامنة ، فتأكد من وجود انتظار!


كما ناقشنا ، عندما يجد المترجم طريقة غير متزامنة ، فإنه يحول هذه الطريقة إلى آلة حالة. إذا لم يكن الكود الخاص بك ينتظر في جسمه ، فسيقوم المترجم بإنشاء تحذير ، ولكن مع ذلك سيتم إنشاء جهاز الحالة ، مع إضافة حمل غير ضروري لعملية لن تكتمل أبدًا.

تجنب الفراغ غير المتزامن


فراغ غير متزامن هو شيء يجب تجنبه حقًا. اجعلها قاعدة لاستخدام مهمة غير متزامنة بدلاً من فراغ غير متزامن.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

طرق المهام المتزامنة باطلة وغير متزامنة

هناك عدة أسباب لذلك ، بما في ذلك:

  • لا يمكن اكتشاف الاستثناءات التي تم طرحها في طريقة إلغاء التزامن خارج هذه الطريقة :
عندما يتم طرح استثناء من أسلوب async Task أو async Task <T >، يتم اكتشاف هذا الاستثناء ووضعه في كائن Task. عند استخدام طرق عدم التزامن ، يكون كائن المهمة غائبًا ، لذلك ، سيتم استدعاء أي استثناءات تم طرحها من طريقة إلغاء التزامن مباشرةً في SynchronizationContext ، الذي كان نشطًا عند تشغيل طريقة إلغاء التزامن.

خذ بعين الاعتبار المثال أدناه. لن يتم الوصول إلى كتلة الالتقاط.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

لا يمكن اكتشاف الاستثناءات التي تم طرحها في طريقة إلغاء التزامن خارج هذه الطريقة.

قارن مع هذا الرمز ، حيث بدلاً من عدم التزامن لدينا مهمة غير متزامنة. في هذه الحالة ، سيتم الوصول إلى الصيد.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

يتم اكتشاف الاستثناء ووضعه في كائن المهام.

  • يمكن أن تتسبب طرق الفراغ غير المتزامن في حدوث آثار جانبية غير مرغوب فيها إذا لم يتوقع المتصل أن تكون غير متزامنة : إذا لم تُرجع طريقتك غير المتزامنة أي شيء ، فاستخدم مهمة غير متزامنة (بدون " <T >" للمهمة) كنوع الإرجاع.
  • من الصعب جدًا اختبار طرق الفراغ غير المتزامن : نظرًا للاختلافات في معالجة الأخطاء والتخطيط ، يصعب كتابة اختبارات الوحدة التي تستدعي طرق الفراغ غير المتزامن. غير المتزامن MSTest اختبار يعمل فقط لأساليب غير متزامن التي ترجع مهمة أو مهمة <T >.

استثناء لهذه الممارسة معالجات الأحداث غير المتزامنة. ولكن حتى في هذه الحالة ، يوصى بتقليل الشفرة المكتوبة في المعالج نفسه - توقع طريقة مهمة غير متزامنة تحتوي على منطق.

مزيد من المعلومات في هذه المقالة MSDN.

تفضل مهمة الإرجاع بدلاً من انتظار العودة


كما تمت مناقشته بالفعل ، في كل مرة تعلن فيها أن إحدى الطرق غير متزامنة ، ينشئ المحول البرمجي فئة آلة حالة تلتف فعليًا منطق أسلوبك. وهذا يضيف بعض النفقات العامة التي يمكن أن تتراكم ، خاصة بالنسبة للأجهزة المحمولة ، حيث لدينا حدود أكثر صرامة للموارد.

في بعض الأحيان ، لا يجب أن تكون الطريقة غير متزامنة ، ولكنها تُرجع المهمة <T >وتسمح للجانب الآخر بمعالجتها وفقًا لذلك. إذا كانت الجملة الأخيرة من التعليمات البرمجية الخاصة بك عبارة عن عودة منتظرة ، فيجب التفكير في إعادة هيكلتها بحيث يكون نوع الإرجاع للطريقة هو المهمة <T>(بدلاً من عدم التزامن T). وبسبب هذا ، تتجنب إنشاء جهاز حالة ، مما يجعل التعليمات البرمجية الخاصة بك أكثر مرونة. الحالة الوحيدة التي نرغب حقًا في انتظارها هي عندما نفعل شيئًا مع نتيجة مهمة غير متزامنة في استمرار الطريقة.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

تفضيل مهمة الإرجاع بدلاً من العودة في انتظار

الملاحظة لاحظ أنه إذا لم يكن لدينا انتظار وبدلاً من إرجاع المهمة <T >، فسيتم الإرجاع على الفور ، لذلك إذا كان الرمز داخل كتلة محاولة / التقاط ، فلن يتم اكتشاف الاستثناء. وبالمثل ، إذا كان الرمز داخل كتلة الاستخدام ، فسيحذف الكائن على الفور. انظر النصيحة التالية.

لا تلف مهمة الإرجاع داخل try..catch {} أو استخدام {} block


يمكن أن تتسبب مهمة الإرجاع في سلوك غير محدد عند استخدامها داخل كتلة try..catch (لن يتم اكتشاف استثناء تم طرحه بواسطة الطريقة غير المتزامنة مطلقًا) أو داخل كتلة تستخدم ، لأنه سيتم إرجاع المهمة على الفور.

إذا كنت بحاجة إلى التفاف التعليمات البرمجية غير المتزامنة في محاولة .. التقاط أو استخدام كتلة ، استخدم العودة في انتظار بدلاً من ذلك.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

لا تلف مهمة الإرجاع داخل الكتل try..catch{}أوusing{} .

مزيد من المعلومات في هذا الموضوع حول تجاوز سعة المكدس.

تجنب استخدام .Wait()أو .Result- استخدم بدلاً من ذلكGetAwaiter().GetResult()


إذا كنت بحاجة إلى منع انتظار التزامن المهام لكامل، واستخدام GetAwaiter().GetResult(). Waitو Resultرمي أي استثناءات في AggregateException، مما يعقد معالجة الأخطاء. الميزة GetAwaiter().GetResult()هي أنه يعيد الاستثناء المعتاد بدلاً من ذلك AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

إذا كنت بحاجة إلى حظر انتظار اكتمال مهمة Async ، فاستخدم GetAwaiter().GetResult().

مزيد من المعلومات على هذا الرابط .

إذا كانت الطريقة غير متزامنة ، قم بإضافة لاحقة Async إلى اسمها


هذا هو الاصطلاح المستخدم في .NET للتمييز بسهولة أكبر بين الطرق المتزامنة وغير المتزامنة (باستثناء معالجات الأحداث أو طرق تحكم الويب ، ولكن لا يزال لا ينبغي أن يتم استدعاؤها بشكل صريح بواسطة التعليمات البرمجية الخاصة بك).

يجب أن تستخدم أساليب المكتبة غير المتزامنة Task.ConfigureAwait (false) لتحسين الأداء



يحتوي .NET Framework على مفهوم "سياق المزامنة" ، وهو وسيلة "للعودة إلى ما كنت عليه من قبل". عندما تنتظر المهمة ، فإنها تلتقط سياق المزامنة الحالي قبل الانتظار.

بعد اكتمال المهمة .Post()، يتم استدعاء أسلوب سياق المزامنة ، الذي يستأنف العمل من المكان الذي كان عليه من قبل. يفيد ذلك في العودة إلى مؤشر ترابط واجهة المستخدم أو في العودة إلى سياق ASP.NET نفسه ، إلخ.
عند كتابة رمز المكتبة ، نادرًا ما تحتاج إلى العودة إلى السياق الذي كنت فيه من قبل. عند استخدام Task.ConfigureAwait (false) ، لم يعد الرمز يحاول الاستئناف من المكان الذي كان عليه من قبل ، وبدلاً من ذلك ، إن أمكن ، يخرج الرمز في سلسلة المحادثات التي أكملت المهمة ، والتي تتجنب تبديل السياق. هذا يحسن الأداء قليلاً ويمكن أن يساعد في تجنب الجمود.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

عادةً ، استخدم ConfigureAwait (false) لعمليات الخادم ورمز المكتبة.
هذا مهم بشكل خاص عندما تسمى طريقة المكتبة بعدد كبير من المرات ، من أجل استجابة أفضل.

بشكل عام ، استخدم ConfigureAwait (false) لعمليات الخادم بشكل عام. نحن لا نهتم بسلسلة المحادثات التي يتم استخدامها للمتابعة ، على عكس التطبيقات التي نحتاج فيها للعودة إلى سلسلة رسائل واجهة المستخدم.

الآن ... في ASP.NET Core ، تخلصت Microsoft من SynchronizationContext ، لذا لا تحتاج نظريًا لذلك. ولكن إذا كنت تكتب رمزًا للمكتبة يُحتمل إعادة استخدامه في تطبيقات أخرى (مثل تطبيق واجهة المستخدم ، و Legacy ASP.NET ، و Xamarin Forms) ، فإن ذلك يظل أفضل ممارسة .

للحصول على شرح جيد لهذا المفهوم ، شاهد هذا الفيديو .

تقرير تقدم المهمة غير المتزامن


إحدى حالات الاستخدام الشائعة إلى حد ما للطرق غير المتزامنة هي العمل في الخلفية ، وتحرير مؤشر ترابط واجهة المستخدم للمهام الأخرى ، والحفاظ على الاستجابة. في هذا السيناريو ، قد ترغب في الإبلاغ عن التقدم مرة أخرى إلى واجهة المستخدم حتى يتمكن المستخدم من مراقبة تقدم العملية والتفاعل مع العملية.

لحل هذه المشكلة الشائعة ، يوفر .NET واجهة IProgress <T >، والتي توفر طريقة Report <T >، التي يتم استدعاؤها بواسطة مهمة غير متزامنة لإبلاغ التقدم إلى المتصل. يتم قبول هذه الواجهة كمعلمة للطريقة غير المتزامنة - يجب على المتصل توفير كائن يقوم بتنفيذ هذه الواجهة.

يوفر .NET Progress <T >، التنفيذ الافتراضي لـ IProgress <T >، وهو أمر موصى به في الواقع ، حيث يتعامل مع جميع منطق المستوى المنخفض المرتبط بحفظ سياق المزامنة واستعادته. تقدم Progress <T >أيضًا حدث Action <T ورد الاتصال >- كلاهما يُدعى عند تقدم مهمة التقارير.

معًا ، يوفر كل من IProgress <T >و Progress <T >طريقة سهلة لنقل معلومات التقدم من مهمة خلفية إلى مؤشر ترابط واجهة المستخدم.

يرجى ملاحظة أن <T>يمكن أن تكون قيمة بسيطة ، مثل int أو كائن يوفر معلومات تقدم سياقية ، مثل النسبة المئوية للاكتمال ، ووصف سلسلة للعملية الحالية ، و ETA ، وما إلى ذلك.
ضع في اعتبارك عدد المرات التي أبلغت فيها عن التقدم. اعتمادًا على العملية التي تقوم بها ، قد تجد أن تقارير التعليمات البرمجية الخاصة بك تتقدم عدة مرات في الثانية ، مما قد يؤدي إلى أن تصبح واجهة المستخدم أقل استجابة. في مثل هذا السيناريو ، يوصى بالإبلاغ عن التقدم على فترات أكبر.

مزيد من المعلومات في هذه المقالة على مدونة Microsoft .NET الرسمية.

إلغاء المهام غير المتزامنة


حالة أخرى شائعة الاستخدام لمهام الخلفية هي القدرة على إلغاء التنفيذ. يوفر .NET فئة CancellationToken. تستقبل الطريقة غير المتزامنة الكائن CancellationToken ، الذي يتم بعد ذلك مشاركته بواسطة رمز الطرف الطالب والطريقة غير المتزامنة ، وبالتالي توفير آلية لإلغاء الإشارة.

في الحالة الأكثر شيوعًا ، يحدث الإلغاء على النحو التالي:

  1. يقوم المتصل بإنشاء كائن CancellationTokenSource.
  2. يتصل المتصل بواجهة برمجة التطبيقات غير المتزامنة الملغاة ويمرر CancellationToken من CancellationTokenSource (CancellationTokenSource.Token).
  3. يطلب المتصل إلغاء باستخدام كائن CancellationTokenSource (CancellationTokenSource.Cancel ()).
  4. تؤكد المهمة الإلغاء وتلغي نفسها ، عادةً باستخدام طريقة CancellationToken.ThrowIfCancellationRequested.

يرجى ملاحظة أنه لكي تعمل هذه الآلية ، ستحتاج إلى كتابة رمز للتحقق من الإلغاءات المطلوبة على فترات منتظمة (أي عند كل تكرار لكودك أو عند نقطة توقف طبيعية في المنطق). من الناحية المثالية ، بعد طلب إلغاء ، يجب إلغاء المهمة غير المتزامنة في أسرع وقت ممكن.

يجب أن تفكر في استخدام التراجع لجميع الطرق التي قد تستغرق وقتًا طويلاً لإكمالها.

مزيد من المعلومات في هذه المقالة على مدونة Microsoft .NET الرسمية.

تقرير التقدم والإلغاء - مثال


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

الانتظار لفترة من الزمن


إذا كنت بحاجة إلى الانتظار لبعض الوقت (على سبيل المثال ، حاول مرة أخرى للتحقق من توفر المورد) ، فتأكد من استخدام Task.Delay - لا تستخدم أبدًا Thread.Sleep في هذا السيناريو.

في انتظار اكتمال العديد من المهام غير المتزامنة


استخدم Task.WaitAny لانتظار اكتمال أي مهمة. استخدم Task.WaitAll في انتظار اكتمال جميع المهام.

هل يجب أن أتسرع في التحول إلى C # 7 أو 8؟ قم بالتسجيل في ندوة مجانية لمناقشة هذا الموضوع.

All Articles