.NET: أدوات للعمل مع multithreading و التزامن. الجزء 1

أنشر المقال الأصلي عن هبر ، والذي يتم نشر ترجمته على مدونة Codingsight .
الجزء الثاني متاح هنا.

كانت الحاجة إلى القيام بشيء ما بشكل غير متزامن ، دون انتظار النتيجة هنا والآن ، أو لمشاركة الكثير من العمل بين عدة وحدات تقوم به ، حتى قبل ظهور أجهزة الكمبيوتر. مع ظهورها ، أصبحت هذه الحاجة ملموسة للغاية. الآن ، في عام 2019 ، اكتب هذه المقالة على جهاز كمبيوتر محمول مع معالج Intel Core ذي 8 نواة ، والذي لا تعمل مائة عملية في نفس الوقت ، ولكن حتى المزيد من سلاسل العمليات. إلى جانب ذلك ، يوجد هاتف مزعج قليلاً ، تم شراؤه قبل عامين ، مع معالج ذي 8 مراكز. الموارد المواضيعية مليئة بالمقالات ومقاطع الفيديو حيث يعجب مؤلفوها بالهواتف الذكية الرائدة لهذا العام حيث يضعون معالجات 16 نواة. بأقل من 20 دولارًا في الساعة ، يوفر MS Azure جهازًا افتراضيًا به 128 معالجات أساسية وذاكرة الوصول العشوائي بسعة 2 تيرابايت. لسوء الحظ ، من المستحيل تعظيم هذه القوة وكبحها دون التمكن من التحكم في تفاعل التدفقات.

مصطلحات


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

استعارة


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

الطبخ الإفطار في الصباح ( وحدة المعالجة المركزية ) أتيت إلى المطبخ ( الكمبيوتر ). لدي 2 الأيدي ( النوى ). يحتوي المطبخ على عدد من الأجهزة ( IO ): فرن ، غلاية ، محمصة ، ثلاجة. أقوم بتشغيل الغاز ، ووضع مقلاة عليه واسكب الزيت فيه ، دون انتظار حتى ترتفع درجة حرارته ( بشكل غير متزامن ، غير مانع للإغلاق ، انتظر ) ، أخرج البيض من الثلاجة وأكسرها في طبق ، ثم أضربها بيد واحدة (مؤشر الترابط # 1) ) ، والثاني ( الموضوع رقم 2 ) أنا أمسك لوحة (الموارد المشتركة). الآن ما زلت أشغل الغلاية ، لكن لا يوجد عدد كافٍ من الأيدي ( تجويع الخيوط ) خلال هذا الوقت ، يتم تسخين المقلاة (معالجة النتيجة) حيث أسكب ما جلدته. لقد وصلت إلى إبريق الشاي وأشغّله وأشاهد بغباء كيف يغلي الماء فيه ( Blocking-IO-Wait ) ، على الرغم من أنني يمكن أن أغسل الصفيحة خلال هذا الوقت ، حيث تغلبت على الأومليت.

لقد طبخت عجة باستخدام يدان فقط ، ولم يكن عندي أكثر من ذلك ، ولكن في نفس الوقت ، حدثت ثلاث عمليات في لحظة ضرب العجة: قذف العجة وعقد طبق وتسخين مقلاة ، وحدة المعالجة المركزية هي أسرع جزء في الكمبيوتر ، IO يبطئ كل شيء ، في كثير من الأحيان حل فعال هو أن تأخذ شيئا وحدة المعالجة المركزية أثناء تلقي البيانات من IO.

استمرار الاستعارة:

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

أدوات .NET


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

بالأدوات ، أعني كلاً من واجهات البرنامج (APIs) التي يوفرها إطار العمل وحزم الطرف الثالث ، والحل البرمجي بأكمله الذي يبسط البحث عن أي مشاكل مرتبطة بالشفرة متعددة الخيوط.

بدء الدفق


فئة مؤشر الترابط ، الفئة الأساسية في .NET للعمل مع مؤشرات الترابط. يقبل المنشئ أحد المندوبين:

  • ThreadStart - لا معلمات
  • ParametrizedThreadStart - مع معلمة واحدة من كائن نوع.

سيتم تنفيذ المفوض في سلسلة الرسائل التي تم إنشاؤها حديثًا بعد استدعاء الأسلوب Start ، إذا تم نقل مفوض من نوع ParametrizedThreadStart إلى المُنشئ ، ثم يجب تمرير كائن إلى طريقة Start. هذه الآلية ضرورية لنقل أي معلومات محلية إلى الدفق. تجدر الإشارة إلى أن إنشاء دفق هو عملية باهظة الثمن ، والدفق نفسه كائن ثقيل ، على الأقل بسبب تخصيص 1 ميغابايت من الذاكرة للمجموعة ، ويتطلب التفاعل مع واجهة برمجة تطبيقات OS.

new Thread(...).Start(...); 

تمثل فئة ThreadPool مفهوم التجمع. في .NET ، يعتبر تجمع مؤشرات الترابط عملًا فنيًا وقد بذل المطورون من Microsoft الكثير من الجهد لجعله يعمل على النحو الأمثل في مجموعة واسعة من السيناريوهات.

المفهوم العام:

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

لاستخدام مؤشر ترابط من التجمع ، هناك طريقة QueueUserWorkItem التي تقبل مفوض WaitCallback ، وهو نفس توقيع ParametrizedThreadStart ، والمعلمة التي تم تمريرها إليها تقوم بنفس الوظيفة.

 ThreadPool.QueueUserWorkItem(...); 

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

 ThreadPool.RegisterWaitForSingleObject(...) 

يحتوي .NET على مؤقت دفق ويختلف عن مؤقتات WinForms / WPF حيث سيتم استدعاء معالجه في دفق مأخوذ من التجمع.

 System.Threading.Timer 

هناك أيضًا طريقة غريبة لإرسال مفوض إلى سلسلة الرسائل من التجمع - طريقة BeginInvoke.

 DelegateInstance.BeginInvoke 

أريد أيضًا أن أسهب في تمرير وظيفة تستدعي العديد من الأساليب المذكورة أعلاه - CreateThread من Kernel32.dll Win32 API. هناك طريقة ، وذلك بفضل آلية الأساليب الخارجية ، لاستدعاء هذه الوظيفة. لقد رأيت مثل هذا التحدي مرة واحدة فقط في مثال فظيع للرمز القديم ، ودافع المؤلف لفعل ذلك لا يزال لغزا بالنسبة لي.

 Kernel32.dll CreateThread 

عرض وتصحيح المواضيع


يمكن عرض مؤشرات الترابط التي قمت بإنشائها شخصيًا بواسطة كافة مكونات الجهات الخارجية وتجمع .NET في إطار مؤشرات الترابط Visual Studio. ستعرض هذه النافذة معلومات حول التدفقات فقط عندما يكون التطبيق قيد التصحيح وفي وضع التوقف (وضع التوقف). هنا يمكنك بسهولة عرض أسماء مكدس وأولويات كل مؤشر ترابط ، والتبديل التصحيح إلى موضوع معين. تتيح لك خاصية Priority للفئة Thread تعيين أولوية الخيط ، والتي سوف ينظر إليها OC و CLR كتوصية عند تقسيم وقت وحدة المعالجة المركزية بين سلاسل العمليات.



مكتبة مهمة متوازية


ظهرت مكتبة Parallel Task (TPL) في .NET 4.0. الآن أصبح المعيار والأداة الرئيسية للعمل مع عدم التزامن. أي رمز باستخدام النهج القديم يعتبر إرثا. الوحدة الأساسية لـ TPL هي فئة المهام من مساحة الاسم System.Threading.Tasks. المهمة تجريد عبر سلسلة. مع الإصدار الجديد من C # ، حصلنا على طريقة أنيقة للعمل مع مشغلي Task - async / انتظار. مكّنت هذه المفاهيم من كتابة التعليمات البرمجية غير المتزامنة كما لو كانت بسيطة ومتزامنة ، مما أتاح حتى للأشخاص الذين لا يفهمون سوى القليل من المطبخ الداخلي للخيوط كتابة التطبيقات التي تستخدمها ، والتطبيقات التي لا تتوقف أثناء العمليات الطويلة. يعد استخدام async / await موضوعًا لمقالة أو حتى عدة مقالات ، لكنني سأحاول الحصول على جوهر بضع جمل:

  • async هو معدل للأسلوب إرجاع المهمة أو الفراغ
  • وينتظر بيان مهمة عدم حظر المهام.

مرة أخرى: سيصدر المشغل الذي ينتظر ، في الحالة العامة (توجد استثناءات) ، مؤشر الترابط الحالي للتنفيذ بشكل أكبر ، وعندما تنتهي المهمة من التنفيذ ، وسيكون مؤشر الترابط (في الحقيقة أنه من الأصح قول السياق ، ولكن المزيد عن ذلك لاحقًا) سيكون حرا في متابعة الطريقة أكثر. داخل .NET ، يتم تنفيذ هذه الآلية بنفس طريقة إرجاع العائد ، عندما تتحول الطريقة المكتوبة إلى فئة كاملة ، وهي عبارة عن آلة حالة ويمكن تنفيذها في أجزاء منفصلة حسب هذه الحالات. يمكن لأي شخص مهتم كتابة أي تعليمات برمجية بسيطة باستخدام asyn / انتظار وتجميع وعرض التجميع باستخدام JetBrains dotPeek مع تمكين Compiler Generated Code.

النظر في الخيارات لبدء واستخدام المهمة. باستخدام مثال التعليمة البرمجية أدناه ، نقوم بإنشاء مهمة جديدة لا تفيد شيئًا ( Thread.Sleep (10000) ) ، ولكن في الواقع يجب أن تكون نوعًا من العمل المعقد الذي يتضمن وحدة المعالجة المركزية.

 using TCO = System.Threading.Tasks.TaskCreationOptions; public static async void VoidAsyncMethod() { var cancellationSource = new CancellationTokenSource(); await Task.Factory.StartNew( // Code of action will be executed on other context () => Thread.Sleep(10000), cancellationSource.Token, TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness, scheduler ); // Code after await will be executed on captured context } 

يتم إنشاء المهمة مع عدد من الخيارات:

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

المعلمة الثانية إلى الأسلوب مرت CancellationToken. من أجل معالجة إلغاء عملية ما بشكل صحيح بعد إطلاقها ، يجب تعبئة الكود الذي تم تنفيذه بالتحقق من حالة إلغاء الحساب. في حالة عدم وجود عمليات تحقق ، فإن طريقة "إلغاء" التي تم استدعاؤها على كائن CancellationTokenSource ستكون قادرة على إيقاف تنفيذ المهمة قبل أن تبدأ.

مرت المعلمة الأخيرة كائن المجدول من نوع TaskScheduler. تم تصميم هذه الفئة وأحفادها للتحكم في استراتيجيات توزيع Task'ov حسب الخيط ، وسيتم تنفيذ المهمة افتراضيًا على خيط عشوائي من المجموعة.

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

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

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

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

 public static async void AnotherMethod() { int result = await AsyncMethod(); // good result = AsyncMethod().Result; // bad AsyncMethod().Wait(); // bad IEnumerable<Task> tasks = new Task[] { AsyncMethod(), OtherAsyncMethod() }; await Task.WhenAll(tasks); // good await Task.WhenAny(tasks); // good Task.WaitAll(tasks.ToArray()); // bad } 

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

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

عيب آخر في هذا النهج هو معالجة الأخطاء المعقدة. الحقيقة هي أن الأخطاء في التعليمات البرمجية غير المتزامنة عند استخدام async / await سهلة للغاية - فهي تتصرف كما لو كانت الشفرة متزامنة. بينما إذا طبقنا طرد الأرواح الشريرة والتوقع المتزامن على المهمة ، فإن الاستثناء الأصلي يتحول إلى AggregateException ، أي للتعامل مع استثناء ، يجب عليك فحص نوع InnerException وكتابة سلسلة if داخل كتلة catch واحدة أو استخدام catch عند الإنشاء بدلاً من سلسلة كتلة catch الشائعة في C #.

يتم تمييز الأمثلة الثالثة والأخيرة أيضًا على أنها سيئة لنفس السبب وتحتوي على نفس المشكلات.

عندما تكون أساليب AnyA و WhenAll مريحة للغاية في انتظار مجموعة من Task'ov ، فإنهم يلفون مجموعة من Task'ov في واحدة ، والتي ستعمل إما على أول عملية من Task'a من المجموعة ، أو عندما يكمل الجميع تنفيذها.

توقف التدفق


لأسباب مختلفة ، قد يكون من الضروري إيقاف الدفق بعد بدء تشغيله. هناك عدة طرق للقيام بذلك. فئة مؤشر الترابط له طريقتان مع أسماء مناسبة - إحباط و المقاطعة . الأول لا ينصح للاستخدام ، كما بعد أن يتم استدعاؤه في أي لحظة عشوائية ، أثناء معالجة أي تعليمات ، سيتم طرح ThreadAbortedException . لا تتوقع تعطل هذا الاستثناء عند زيادة متغير عدد صحيح ، أليس كذلك؟ وعند استخدام هذه الطريقة ، هذا وضع حقيقي للغاية. إذا كنت ترغب في منع CLR من رمي مثل هذا الاستثناء في قسم معين من التعليمات البرمجية ، يمكنك لفه في المكالمات إلى Thread.BeginCriticalRegion و Thread.EndCriticalRegion . أي رمز مكتوب في كتلة أخيرًا يلف بمثل هذه المكالمات. لهذا السبب ، في أحشاء رمز الإطار ، يمكنك العثور على كتل ذات محاولة فارغة ، ولكن ليس فارغة في النهاية. لا توصي Microsoft باستخدام هذه الطريقة حتى لا يتم تضمينها في .net core.

أسلوب المقاطعة يعمل بشكل أكثر توقعًا. يمكن مقاطعة مؤشر ترابط باستثناء ThreadInterruptedException فقط عندما يكون مؤشر الترابط في حالة الخمول. في هذه الحالة ، يتم تعليقه أثناء انتظار WaitHandle أو القفل أو بعد استدعاء Thread.Sleep.

كلا الخيارين الموصوفين أعلاه سيئين لعدم القدرة على التنبؤ بهما. الحل هو استخدام بنية CancellationToken وفئة CancellationTokenSource . خلاصة القول هي: يتم إنشاء مثيل للفئة CancellationTokenSource ويمكن فقط للشخص الذي يمتلك إيقاف العملية عن طريق استدعاء الأسلوب Cancel . فقط يتم إلغاء CancellationToken إلى العملية نفسها. لا يمكن لمالكي CancellationToken إلغاء العملية بأنفسهم ، لكن يمكنهم التحقق فقط من إلغاء العملية. للقيام بذلك ، هناك خاصية منطقية IsCancellationRequested وطريقة ThrowIfCancelRequested . سيقوم الأخير برفع TaskCancelledException إذا تم استدعاء أسلوب "الإلغاء" على مثيل CancellationToken الذي تم إلغاؤه من CancellationTokenSource. وهذه هي الطريقة التي أوصي باستخدام. هذا أفضل من الخيارات السابقة من خلال التحكم الكامل في النقاط التي يمكن مقاطعة عملية الاستثناء فيها.

الخيار الأكثر قسوة لإيقاف مؤشر الترابط هو استدعاء Win32 API TerminateThread وظيفة. يمكن أن يكون سلوك CLR بعد استدعاء هذه الوظيفة غير متوقع. على MSDN ، يتم كتابة ما يلي حول هذه الوظيفة: "TerminateThread هي وظيفة خطيرة يجب استخدامها فقط في الحالات القصوى. "

تحويل legacy-API إلى Task Based باستخدام FromAsync method


إذا كنت محظوظًا بما فيه الكفاية للعمل في مشروع بدأ بعد عرض المهام وتوقف عن التسبب في رعب هادئ لمعظم المطورين ، فلن تضطر إلى التعامل مع الكثير من واجهات برمجة التطبيقات القديمة ، سواء من طرف ثالث أو فريقك في الماضي. لحسن الحظ ، اعتنى بنا فريق تطوير .NET Framework ، على الرغم من أن الهدف ربما كان الاعتناء بأنفسنا. بصرف النظر عن ذلك ، لدى .NET عدد من الأدوات لتحويل التعليمات البرمجية غير المؤلمة المكتوبة في أساليب البرمجة غير المتزامنة القديمة إلى أخرى جديدة. واحد منهم هو الأسلوب FromAsync من TaskFactory. باستخدام مثال التعليمة البرمجية أدناه ، أقوم بالالتفاف على الأساليب غير المتزامنة القديمة لفئة WebRequest في المهام باستخدام هذه الطريقة.

 object state = null; WebRequest wr = WebRequest.CreateHttp("http://github.com"); await Task.Factory.FromAsync( wr.BeginGetResponse, we.EndGetResponse ); 

هذا مجرد مثال ، ومن غير المحتمل أن تقوم بذلك بأنواع مضمّنة ، لكن أي مشروع قديم يعج ببساطة بأساليب BeginDoSomething التي تعيد أساليب IAsyncResult و EndDoSomething التي تقبلها.

تحويل legacy-API إلى Task Based باستخدام TaskCompletionSource class


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

ستقول أنني تحدثت بالفعل عن أسلوب FromAsync لفئة TaskFactory المخصصة لهذه الأغراض. هنا سيتعين علينا أن نتذكر التاريخ الكامل لتطوير النماذج غير المتزامنة في .net التي قدمتها Microsoft على مدار الخمسة عشر عامًا الماضية: قبل نمط غير متزامن القائم على المهام (TAP) ، كانت هناك أنماط البرمجة غير المتزامنة (APP) ، والتي كانت تدور حول أساليب Begin DoSomething التي ترجع طرق IAsyncResult و End DoSomething التي تقبلها وأسلوب FromAsync جيد تمامًا لإرث هذه السنوات ، ولكن بمرور الوقت ، تم استبداله بنمط غير متزامن قائم على الأحداث ( EAP ) ، والذي افترض أنه سيتم استدعاء حدث عند إتمام العملية غير المتزامنة.

يعتبر TaskCompletionSource رائعًا للالتفاف في Task و API القديمة الموروثة حول نموذج الحدث. جوهر عمله كما يلي: يحتوي كائن من هذه الفئة على خاصية عامة من النوع Task يمكن التحكم في حالته من خلال أساليب SetResult و SetException وما إلى ذلك من فئة TaskCompletionSource. في الأماكن التي تم فيها تطبيق عامل الانتظار على هذه المهمة ، سيتم تنفيذه أو تعطله مع استثناء ، وفقًا للطريقة المطبقة على TaskCompletionSource. إذا كان كل شيء لا يزال غير واضح ، فلنلقِ نظرة على مثال التعليمة البرمجية هذا ، حيث يتم التفاف بعض واجهات برمجة تطبيقات EAP القديمة في Task باستخدام TaskCompletionSource: عند إطلاق الحدث ، سيتم نقل المهمة إلى الحالة مكتملة ، وستستأنف الطريقة التي طبقت عامل انتظار الانتظار على هذه المهمة. الحصول على كائن النتيجة .

 public static Task<Result> DoAsync(this SomeApiInstance someApiObj) { var completionSource = new TaskCompletionSource<Result>(); someApiObj.Done += result => completionSource.SetResult(result); someApiObj.Do(); result completionSource.Task; } 

TaskCompletionSource نصائح والخدع


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

باختصار ، فإن جوهر الخدعة هو: تحتاج إلى الحصول على معلومات من واجهة برمجة التطبيقات حول بعض الأحداث التي تحدث من جانبها ، في حين أن واجهة برمجة التطبيقات لسبب ما لا يمكنها الإبلاغ عن الحدث ، لكن يمكنها فقط إعادة الحالة. مثال على ذلك هو جميع واجهات برمجة التطبيقات التي بنيت على رأس HTTP قبل أوقات WebSocket أو عندما يكون من المستحيل لسبب ما استخدام هذه التكنولوجيا. قد يطلب العميل من خادم HTTP. لا يمكن لخادم HTTP نفسه إثارة التواصل مع العميل. يتمثل الحل البسيط في استجواب الخادم عن طريق المؤقت ، ولكن هذا يخلق حملًا إضافيًا على الخادم وتأخيرًا إضافيًا في المتوسط ​​TimerInterval / 2. للتغلب على ذلك ، تم اختراع خدعة تدعى Long Polling ، والتي تتضمن تأخير الاستجابة من الخادم حتى انتهاء المهلة أو حدث سيحدث. إذا حدث حدث ما ، فستتم معالجته ؛ وإذا لم يحدث ذلك ، فسيتم إرسال الطلب مرة أخرى.

 while(!eventOccures && !timeoutExceeded) { CheckTimout(); CheckEvent(); Thread.Sleep(1); } 

ولكن مثل هذا الحل سيظهر نفسه بشكل رهيب بمجرد زيادة عدد العملاء الذين ينتظرون الحدث ، لأن كل عميل من هذا القبيل ، تحسبا لهذا الحدث ، يأخذ مجرى كامل. نعم ، وحصلنا على تأخير إضافي قدره دقيقة واحدة عند بدء الحدث ، وغالبًا ما يكون هذا غير مهم ، ولكن لماذا جعل البرنامج أسوأ مما يمكن أن يكون؟ إذا قمت بإزالة Thread.Sleep (1) ، فمن دون جدوى سنقوم بتحميل نواة معالج واحدة بنسبة 100٪ في وضع الخمول ، في دورة غير مجدية. باستخدام TaskCompletionSource ، يمكنك بسهولة إعادة هذا الرمز وحل جميع المشكلات المحددة أعلاه:

 class LongPollingApi { private Dictionary<int, TaskCompletionSource<Msg>> tasks; public async Task<Msg> AcceptMessageAsync(int userId, int duration) { var cs = new TaskCompletionSource<Msg>(); tasks[userId] = cs; await Task.WhenAny(Task.Delay(duration), cs.Task); return cs.Task.IsCompleted ? cs.Task.Result : null; } public void SendMessage(int userId, Msg m) { if (tasks.TryGetValue(userId, out var completionSource)) completionSource.SetResult(m); } } 

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

عندما نتلقى طلبًا للحصول على رسالة ، نقوم بإنشاء TaskCompletionSource ووضعه في القاموس ، ثم ننتظر ما يحدث أولاً: انتهاء الفاصل الزمني المحدد أو استلام رسالة.

ValueTask: لماذا وكيف


يقوم المشغلون المتزامنون / المنتظرون ، مثل مشغل إرجاع العائد ، بإنشاء جهاز حالة من الطريقة ، التي تقوم بإنشاء كائن جديد ، وهو أمر غير مهم دائمًا تقريبًا ، ولكن في حالات نادرة ، يمكن أن يحدث مشكلة. قد تكون هذه الحالة طريقة تسمى كثيرًا ، تتحدث عن عشرات ومئات الآلاف من المكالمات في الثانية. إذا تمت كتابة مثل هذه الطريقة بحيث تقوم في معظم الحالات بإرجاع نتيجة تتجاوز جميع طرق الانتظار ، فإن .NET يوفر أداة لتحسين ذلك - بنية ValueTask. لتوضيح الأمر ، فكر في مثال على استخدامه: توجد ذاكرة تخزين مؤقت نذهب إليها كثيرًا. هناك بعض القيم فيه ثم نعيدها فقط ، إن لم يكن ، فسنذهب إلى بعض IO البطيء وراءها. أريد أن أفعل هذا الأخير بشكل غير متزامن ، مما يعني أن الطريقة برمتها غير متزامنة. وبالتالي ، فإن الطريقة الواضحة لكتابة طريقة هي كما يلي:

 public async Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return val; return await RequestById(id); } 

- , - Roslyn , :

 public Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return Task.FromResult(val); return RequestById(id); } 

hot-path, GC, , IO / :

 public ValueTask<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return new ValueTask<string>(val); return new ValueTask<string>(RequestById(id)); } 

: , . : ValueTask C# Task .

TaskScheduler': Task'


واجهة برمجة التطبيقات التالية التي أود دراستها هي فئة TaskScheduler ومشتقاتها. سبق أن ذكرت أعلاه أنه في TPL هناك القدرة على التحكم في استراتيجيات توزيع Task'ov حسب الموضوع. يتم تعريف هذه الاستراتيجيات في أحفاد فئة TaskScheduler. سيتم العثور على أي إستراتيجية قد تحتاجها تقريبًا في مكتبة ParallelExtensionsExtras ، التي طورتها شركة Microsoft ، ولكن ليس كجزء من .NET ، ولكن يتم تسليمها كحزمة Nuget. دعونا نفكر بإيجاز في بعضها:

  • CurrentThreadTaskScheduler - يؤدي مهمة على مؤشر الترابط الحالي
  • LimitedConcurrencyLevelTaskScheduler - يحد من عدد المهام المنفذة في وقت واحد إلى المعلمة N ، والتي يتم قبولها في المُنشئ
  • OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1), .
  • WorkStealingTaskSchedulerwork-stealing . ThreadPool. , .NET ThreadPool , , . . .. WorkStealingTaskScheduler' , ThreadPool .
  • QueuedTaskScheduler - يتيح لك أداء المهام وفقا لقواعد قائمة الانتظار مع الأولويات
  • يقوم ThreadPerTaskScheduler - بإنشاء مؤشر ترابط منفصل لكل مهمة تعمل عليه. يمكن أن يكون مفيدًا للمهام التي تعمل لفترة طويلة بشكل غير متوقع.

توجد مقالة مفصلة جيدة حول TaskSchedulers على مدونة Microsoft.

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



PLinq والطبقة الموازية


Task' .NET PLinq(Linq2Parallel) Parallel. Linq . - WithDegreeOfParallelism. , PLinq , , : AsParallel Linq . PLinq Partitions. .

توفر الفئة الثابتة الموازية طرقًا للتكرار عبر مجموعة Foreach بشكل متوازٍ وتنفيذ حلقة For وتنفيذ مفوضين متعددين بالتوازي مع استدعاء. سيتم إيقاف تنفيذ سلسلة العمليات الحالية حتى نهاية العمليات الحسابية. يمكن تكوين عدد سلاسل العمليات عن طريق تمرير ParallelOptions كوسيطة أخيرة. باستخدام الخيارات ، يمكنك أيضًا تحديد TaskScheduler و CancellationToken.

النتائج


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

الاستنتاجات:

  • تحتاج إلى معرفة الأدوات اللازمة للتعامل مع مؤشرات الترابط والتزامن والتوازي من أجل استخدام موارد أجهزة الكمبيوتر الحديثة.
  • يحتوي .NET على العديد من الأدوات المختلفة لهذا الغرض.
  • لم تظهر جميعها مرة واحدة ، لأنه غالبًا ما يمكن العثور على الإرث ، ولكن هناك طرق لتحويل واجهات برمجة التطبيقات القديمة دون بذل الكثير من الجهد.
  • يمثل العمل مع مؤشرات الترابط في .NET من خلال الفئات Thread و ThreadPool
  • Thread.Abort, Thread.Interrupt, Win32 API TerminateThread . CancellationToken'
  • — , . , . TaskCompletionSource
  • .NET Task'.
  • c# async/await
  • Task' TaskScheduler'
  • ValueTask hot-paths memory-traffic
  • Tasks Threads Visual Studio
  • PLinq , , partitioning
  • ...

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


All Articles