ConfigureAwait ، على من يقع اللوم وماذا يفعل؟

في ممارستي ، غالبًا ما أجد ، في بيئة مختلفة ، رمزًا مثل الرمز أدناه:


[1] var x = FooWithResultAsync(/*...*/).Result; // [2] FooAsync(/*...*/).Wait(); // [3] FooAsync(/*...*/).GetAwaiter().GetResult(); // [4] FooAsync(/*...*/) .ConfigureAwait(false) .GetAwaiter() .GetResult(); // [5] await FooAsync(/*...*/).ConfigureAwait(false) //  [6] await FooAsync(/*...*/) 

من التواصل مع مؤلفي هذه الخطوط ، أصبح من الواضح أنهم منقسمون إلى ثلاث مجموعات:


  • المجموعة الأولى هي أولئك الذين لا يعرفون شيئًا عن المشاكل المحتملة عند استدعاء Result/Wait/GetResult . الأمثلة (1-3) وأحيانًا (6) نموذجية للمبرمجين من هذه المجموعة ؛
  • تتضمن المجموعة الثانية مبرمجين مدركين للمشاكل المحتملة ، لكنهم لا يعرفون أسباب حدوثها. يحاول المطورون من هذه المجموعة ، من ناحية ، تجنب خطوط مثل (1-3 و 6) ، ولكن ، من ناحية أخرى ، رمز الإساءة مثل (4-5) ؛
  • المجموعة الثالثة ، في تجربتي الأصغر ، هي أولئك المبرمجين الذين يعرفون كيفية عمل الكود (1-6) ، وبالتالي يمكنهم اتخاذ قرار مستنير.

هل الخطر ممكن ، وكم هو كبير ، عند استخدام الكود ، كما في الأمثلة أعلاه ، يعتمد ، كما أشرت سابقًا ، على البيئة .



المخاطر وأسبابها


تنقسم الأمثلة (1-6) إلى مجموعتين. المجموعة الأولى هي رمز يمنع سلسلة الاستدعاء. هذه المجموعة تضم (1-4).
غالبًا ما يكون حظر سلسلة الرسائل فكرة سيئة. لماذا؟ من أجل البساطة ، نفترض أن جميع سلاسل العمليات قد تم تخصيصها من مجموعة مؤشرات الترابط. إذا كان البرنامج لديه قفل ، فإن هذا يمكن أن يؤدي إلى اختيار جميع المواضيع من التجمع. في أفضل الأحوال ، سيؤدي ذلك إلى إبطاء البرنامج ويؤدي إلى استخدام غير فعال للموارد. في أسوأ الحالات ، يمكن أن يؤدي هذا إلى حالة توقف تام ، عندما تكون هناك حاجة إلى مؤشر ترابط إضافي لإكمال بعض المهام ، ولكن يتعذر على التجمع تخصيصها.
وبالتالي ، عندما يكتب المطور رمزًا مثل (1-4) ، يجب أن يفكر في مدى احتمال الموقف الموصوف أعلاه.


لكن الأمور تزداد سوءًا عندما نعمل في بيئة يكون فيها سياق التزامن مختلفًا عن المعيار. إذا كان هناك سياق مزامنة خاص ، فإن حظر مؤشر ترابط الاستدعاء يزيد من احتمال حدوث حالة توقف تام عدة مرات. لذلك ، يتم ضمان رمز من الأمثلة (1-3) ، إذا تم تنفيذه في مؤشر ترابط واجهة المستخدم WinForms ، لإنشاء حالة توقف تام. أنا أكتب "عمليا" ل هناك خيار عندما لا يكون الأمر كذلك ، ولكن المزيد عن ذلك لاحقًا. إن إضافة ConfigureAwait(false) ، كما في (4) ، لن يوفر ضمانًا بنسبة 100٪ للحماية من حالة توقف تام. فيما يلي مثال لتأكيد هذا:


 [7] //   /  . async Task FooAsync() { // Delay   .     . await Task.Delay(5000); //       RestPartOfMethodCode(); } //  ""  ,   ,  WinForms . private void button1_Click(object sender, EventArgs e) { FooAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); button1.Text = "new text"; } 

توفر المقالة "الحوسبة المتوازية - كل شيء عن SynchronizationContext" معلومات حول سياقات التزامن المختلفة.


لفهم سبب حالة توقف تام ، تحتاج إلى تحليل رمز جهاز الحالة الذي يتم تحويل الاستدعاء إلى طريقة المزامنة فيه ، ثم رمز فئات MS. توفر مقالة Async Await و Generated StateMachine مثالًا على مثل هذه الحالة.
لن أعطي الشفرة المصدرية الكاملة التي تم إنشاؤها على سبيل المثال (7) ، الأوتوماتون ، سأعرض فقط الخطوط المهمة لمزيد من التحليل:


 //  MoveNext. //... //  taskAwaiter    . taskAwaiter = Task.Delay(5000).GetAwaiter(); if(tasAwaiter.IsCompleted != true) { _awaiter = taskAwaiter; _nextState = ...; _builder.AwaitUnsafeOnCompleted<TaskAwaiter, ThisStateMachine>(ref taskAwaiter, ref this); return; } 

يتم تنفيذ الفرع if إذا لم يتم إكمال الاستدعاء غير المتزامن ( Delay ) وبالتالي ، يمكن تحرير سلسلة الرسائل الحالية.
يرجى ملاحظة أنه في AwaitUnsafeOnCompleted ، يتم استلام FooAsync مكالمة غير متزامنة داخلية (نسبة إلى FooAsync ) ( Delay ).


إذا AwaitUnsafeOnCompleted في غابة مصادر MS المخفية وراء استدعاء AwaitUnsafeOnCompleted ، ثم ، في النهاية ، AwaitUnsafeOnCompleted إلى فئة SynchronizationContextAwaitTaskContinuation ، وفئة AwaitTaskContinuation الخاصة به ، حيث توجد الإجابة على السؤال.


رمز هذه الفئات والفئات ذات الصلة مربكة إلى حد ما ، وبالتالي ، لتسهيل الإدراك ، أسمح لنفسي بكتابة "تناظرية" مبسطة جدًا لما يتحول إليه المثال (7) ، ولكن بدون جهاز دولة ، وفيما يتعلق بـ TPL:


 [8] Task FooAsync() { //  methodCompleted    ,  , //    ,     " ". //    ,   methodCompleted.WaitOne()  , //   SetResult  AsyncTaskMethodBuilder, //       . var methodCompleted = new AutoResetEvent(false); SynchronizationContext current = SynchronizationContext.Current; return Task.Delay(5000).ContinueWith( t=> { if(current == null) { RestPartOfMethodCode(methodCompleted); } else { current.Post(state=>RestPartOfMethodCode(methodCompleted), null); methodCompleted.WaitOne(); } }, TaskScheduler.Current); } // // void RestPartOfMethodCode(AutoResetEvent methodCompleted) // { //      FooAsync. // methodCompleted.Set(); // } 

في المثال (8) ، من المهم الانتباه إلى حقيقة أنه في حالة وجود سياق التزامن ، يتم تنفيذ جميع التعليمات البرمجية الخاصة بالطريقة غير المتزامنة التي تأتي بعد إتمام الاستدعاء الداخلي غير المتزامن من خلال هذا السياق (استدعاء current.Post(...) ). هذه الحقيقة هي سبب الجمود. على سبيل المثال ، إذا كنا نتحدث عن تطبيق WinForms ، فإن سياق المزامنة فيه مرتبط بدفق واجهة المستخدم. إذا تم حظر مؤشر ترابط واجهة المستخدم ، في المثال (7) يحدث ذلك من خلال استدعاء .GetResult() ، فلا يمكن تنفيذ باقي كود الطريقة غير المتزامنة ، مما يعني أنه لا يمكن إكمال الطريقة غير المتزامنة ، ولا يمكن تحرير مؤشر ترابط واجهة المستخدم ، وهو طريق مسدود.


في المثال (7) ، تم تكوين استدعاء FooAsync عبر ConfigureAwait(false) ، لكن هذا لم يساعد. الحقيقة هي أنك تحتاج إلى تكوين كائن الانتظار الذي سيتم تمريره إلى AwaitUnsafeOnCompleted ، في مثالنا ، هذا هو كائن الانتظار من استدعاء Delay . بمعنى آخر ، في هذه الحالة ، استدعاء ConfigureAwait(false) في رمز العميل غير منطقي. يمكنك حل المشكلة إذا قام مطور أسلوب FooAsync كما يلي:


 [9] async Task FooAsync() { await Task.Delay(5000).ConfigureAwait(false); //       RestPartOfMethodCode(); } private void button1_Click(object sender, EventArgs e) { FooAsync().GetAwaiter().GetResult(); button1.Text = "new text"; } 

أعلاه ، قمنا بفحص المخاطر التي تنشأ مع رمز المجموعة الأولى - الكود مع الحظر (الأمثلة 1-4). الآن عن المجموعة الثانية (المثالان 5 و 6) - رمز بدون أقفال. في هذه الحالة ، يكون السؤال هو: متى يتم تبرير الدعوة إلى ConfigureAwait(false) ؟ عند تحليل المثال (7) ، اكتشفنا بالفعل أننا بحاجة إلى تكوين كائن الانتظار الذي سيتم بناء عليه استمرار التنفيذ. أي التكوين مطلوب (إذا اتخذت هذا القرار) فقط للمكالمات غير المتزامنة الداخلية .


على من يقع اللوم؟


كما هو الحال دائمًا ، فإن الإجابة الصحيحة هي "كل شيء". لنبدأ مع المبرمجين من MS. من ناحية ، قرر مطورو Microsoft أنه في حالة وجود سياق التزامن ، يجب تنفيذ العمل من خلاله. وهذا منطقي ، وإلا لماذا لا تزال هناك حاجة إليه. وكما أعتقد ، توقعوا ألا يقوم مطورو رمز "العميل" بمنع الخيط الرئيسي ، خاصةً إذا كان سياق التزامن مرتبطًا به. من ناحية أخرى ، أعطوا أداة بسيطة جدًا "لإطلاق النار على قدمك" - إنها بسيطة ومريحة للغاية للحصول على النتيجة من خلال الحظر .Result/.GetResult ، أو حظر الدفق ، في انتظار انتهاء المكالمة ، من خلال .Wait . أي جعل مطورو MS من الممكن أن الاستخدام "الخاطئ" (أو الخطير) لمكتباتهم لا يسبب أي صعوبات.


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


ما يجب القيام به


أدناه أعطي توصياتي.


لمطوري كود العميل


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

اتخاذ قرار بناء على جميع المعلومات الواردة. قد تحتاج إلى إعادة التفكير في نهج التنفيذ الخاص بك. ربما سوف يساعدك ConfigureAwait ، أو ربما لا تحتاجها.


لمطوري المكتبة


  1. إذا كنت تعتقد أنه يمكن استدعاء الشفرة الخاصة بك من "متزامن" ، فتأكد من تطبيق API متزامن. يجب أن يكون متزامنًا حقًا ، أي يجب عليك استخدام API المتزامن لمكتبات الجهات الخارجية.
  2. ConfigureAwait(true / false) .

هنا ، من وجهة نظري ، هناك حاجة إلى نهج أكثر دقة من الموصى به عادة. تقول العديد من المقالات أنه في رمز المكتبة ، يجب تكوين جميع المكالمات غير المتزامنة من خلال ConfigureAwait(false) . لا أستطيع أن أتفق مع ذلك. ربما ، من وجهة نظر المؤلفين ، اتخذ زملاء من Microsoft القرار الخطأ عند اختيار السلوك "الافتراضي" فيما يتعلق بالعمل مع سياق التزامن. لكنهم (MS) ، مع ذلك ، تركوا الفرصة لمطوري كود "العميل" لتغيير هذا السلوك. تقوم الإستراتيجية ، عندما يتم تغطية رمز المكتبة بالكامل بواسطة ConfigureAwait(false) ، بتغيير السلوك الافتراضي ، والأهم من ذلك ، أن هذا الأسلوب يحرم المطورين من رمز "العميل" المفضل.


الخيار الخاص بي هو ، عند تطبيق API غير المتزامن ، إضافة معلمتين إضافيتين إضافيتين إلى كل طريقة من واجهات برمجة التطبيقات: CancellationToken token and bool continueOnCapturedContext . وتنفيذ الكود كما يلي:


 public async Task<string> FooAsync( /*  */, CancellationToken token, bool continueOnCapturedContext) { // ... await Task.Delay(30, token).ConfigureAwait(continueOnCapturedContext); // ... return result; } 

المعلمة الأولى ، token ، تخدم ، كما تعلمون ، إمكانية الإلغاء المنسق (مطورو المكتبات يهملون هذه الميزة أحيانًا). الثاني ، continueOnCapturedContext onCapturedContext - يسمح لك بتكوين التفاعل مع سياق التزامن للمكالمات غير المتزامنة الداخلية.


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


 //     : async Task ClientFoo() { // ""  ClientFoo   ,     //   FooAsync   . await FooAsync( /*  */, ancellationToken.None, false); //     . await FooAsync( /*  */, ancellationToken.None, false).ConfigureAwait(false); //... } // ,  . private void button1_Click(object sender, EventArgs e) { FooAsync( /*  */, _source.Token, false).GetAwaiter().GetResult(); button1.Text = "new text"; } 

في الختام


الاستنتاج الرئيسي من ما تقدم هو الأفكار الثلاثة التالية:


  1. الأقفال هي في معظم الأحيان أصل كل الشرور. إنه وجود الأقفال التي يمكن أن تؤدي ، في أفضل الأحوال ، إلى تدهور الأداء والاستخدام غير الفعال للموارد ، في أسوأ الأحوال - إلى طريق مسدود. قبل استخدام الأقفال ، فكر فيما إذا كان هذا ضروريًا؟ ربما هناك طريقة أخرى للمزامنة مقبولة في حالتك ؛
  2. تعلم الأداة التي تعمل بها ؛
  3. إذا قمت بتصميم مكتبات ، فحاول أن تتأكد من أن استخدامها الصحيح سهل وبديهي ، وأن الاستخدام الخاطئ محفوف بالتعقيد.

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


سكرتير خاص سأكون ممتنا للنقد البناء.


Source: https://habr.com/ru/post/ar463587/


All Articles