لقد نشرت هذه المقالة في الأصل في مدونة CodingSightالجزء الثاني من المقال متاح هناكانت الحاجة إلى القيام بالأشياء بطريقة غير متزامنة - أي تقسيم المهام الكبيرة بين وحدات عمل متعددة - موجودة قبل وقت طويل من ظهور أجهزة الكمبيوتر. ومع ذلك ، عندما ظهروا ، أصبحت هذه الحاجة أكثر وضوحًا. إنه الآن 2019 ، وأنا أكتب هذه المقالة على كمبيوتر محمول مدعوم من وحدة المعالجة المركزية Intel Core 8 النواة والتي ، بالإضافة إلى ذلك ، تعمل في وقت واحد على مئات العمليات ، مع زيادة عدد الخيوط. بجواري ، يوجد هاتف ذكي قديم الطراز اشتريت منه قبل عامين - ويضم أيضًا معالجًا ذو 8 مراكز. تحتوي موارد الويب المتخصصة على مجموعة واسعة من المقالات التي تشيد بالهواتف الذكية الرائدة لهذا العام والمجهزة بوحدات المعالجة المركزية ذات 16 وحدة. مقابل أقل من 20 دولارًا في الساعة ، يمكن أن يمنحك MS Azure إمكانية الوصول إلى جهاز ظاهري 128 نواة مزود بذاكرة الوصول العشوائي بسعة 2 تيرابايت. لكن لسوء الحظ ، لا يمكنك الحصول على أقصى استفادة من هذه الطاقة إلا إذا كنت تعرف كيفية التحكم في التفاعل بين سلاسل الرسائل.
محتويات
مصطلحات
العملية - كائن OS الذي يمثل مساحة عنوان معزولة تحتوي على مؤشرات ترابط.
مؤشر
ترابط - كائن OS يمثل أصغر وحدة تنفيذ. تعتبر سلاسل العمليات جزءًا أساسيًا من العمليات ، حيث تقسم الذاكرة والموارد الأخرى بين بعضها البعض في نطاق العملية.
تعدد المهام - ميزة نظام التشغيل التي تمثل القدرة على تنفيذ عمليات متعددة في وقت واحد.
متعدد النواة - ميزة وحدة المعالجة المركزية التي تمثل القدرة على استخدام النوى متعددة لمعالجة البيانات
المعالجة المتعددة - ميزة الكمبيوتر الذي يمثل القدرة على العمل الفعلي مع وحدات المعالجة المركزية المتعددة.
تعدد
الخيوط - ميزة العملية التي تمثل إمكانية تقسيم ونشر معالجة البيانات بين عدة خيوط.
التوازي - التنفيذ المادي المتزامن لأفعال متعددة في وحدة زمنية
غير متزامن - تنفيذ عملية ما دون انتظار معالجتها بالكامل ، مع ترك حساب النتيجة لوقت لاحق.
استعارة
ليست كل التعاريف فعالة وبعضها يحتاج إلى توضيح ، لذلك اسمحوا لي أن أقدم استعارة للطهي للمصطلحات التي قدمتها للتو.
صنع الإفطار يمثل
عملية في هذا الاستعارة.
عند إعداد وجبة الإفطار في الصباح ، أذهب (
CPU ) إلى المطبخ (
الكمبيوتر ). لدي يدان (
النوى ). على المطبخ ، هناك مجموعة متنوعة من الأجهزة (
IO ): الموقد ، غلاية ، محمصة ، الثلاجة. أدير الموقد وأضع عليه مقلاة وسكب بعض الزيت النباتي فيه. دون انتظار تسخين الزيت (
بشكل غير متزامن ، عدم حظر إدخال / إخراج ضوئي- انتظر ) ، أحصل على بعض البيض من الثلاجة ، وقم بتكسيرها في وعاء ثم قم بسكها بيد واحدة (مؤشر
الترابط رقم 1 ). في الوقت نفسه ، تمسك اليد الثانية (الخيط رقم 2) بالوعاء في مكانه (
الموارد المشتركة ). أرغب في تشغيل الغلاية ، لكن ليس لدي ما يكفي من الأيدي الحرة في الوقت الحالي (
Thread Starvation ). بينما كنت أضرب البيض ، أصبحت المقلاة ساخنة بدرجة كافية (معالجة النتيجة) ، لذلك أسكب البيض المخفوق فيه. وصلت إلى الغلاية ، وأديرها وألقي نظرة على الماء المغلي (
Blocking-IO-Wait ) - لكن كان بإمكاني استخدام هذا الوقت لغسل الوعاء.
لم أستخدم يدان فقط أثناء صنع العجة (لأنني لا أملك المزيد) ، لكن تم تنفيذ 3 عمليات متزامنة: خفق البيض وعقد الوعاء وتسخين المقلاة. وحدة المعالجة المركزية (CPU) هي أسرع جزء من جهاز الكمبيوتر و IO هو الجزء الذي يتطلب الانتظار في أغلب الأحيان ، لذلك فمن الفعال جدًا تحميل وحدة المعالجة المركزية ببعض الأعمال أثناء انتظار البيانات من IO.
لتمديد الاستعارة:
- إذا كنت أحاول أيضًا تغيير ملابسي أثناء الإفطار ، كنت سأقوم بمهام متعددة . أجهزة الكمبيوتر أفضل في هذا من البشر.
- المطبخ مع طهاة متعددة - على سبيل المثال ، في مطعم - هو جهاز كمبيوتر متعدد النواة .
- تمثل قاعة الطعام في مول بها العديد من المطاعم مركز بيانات .
أدوات .NET
إن .NET جيد حقًا عندما يتعلق الأمر بالعمل مع مؤشرات الترابط - وكذلك في العديد من الأشياء الأخرى. مع كل إصدار جديد ، فإنه يوفر المزيد من الأدوات للعمل مع سلاسل الرسائل وطبقات تجريد سلسلة OS الجديدة. عند العمل مع التجريدات ، يستخدم المطورون العاملون في إطار العمل أسلوبًا يسمح لهم بدفع طبقة واحدة أو أكثر أثناء استخدام التجريدات عالية المستوى. في معظم الحالات ، ليست هناك حاجة حقيقية للقيام بذلك (والقيام بذلك قد يوفر إمكانية إطلاق النار عليك) ، لكن في بعض الأحيان قد يكون هذا هو السبيل الوحيد لحل مشكلة لا يمكن حلها على مستوى التجريد الحالي.
عندما قلت الأدوات في وقت سابق ، كنت أقصد كلاً من واجهات البرنامج (API) التي يوفرها إطار العمل أو حزم الطرف الثالث وحلول البرامج الكاملة التي تعمل على تبسيط عملية البحث عن المشكلات المتعلقة بالكود متعدد الخيوط.
بدء الموضوع
فئة مؤشر الترابط هي الفئة .NET الأساسية للعمل مع مؤشرات الترابط. يقبل منشئها أحد هذين المندوبين:
- ThreadStart - لا معلمات
- ParametrizedThreadStart - معلمة نوع كائن واحد.
سيتم تنفيذ المفوض في سلسلة رسائل تم إنشاؤها حديثًا بعد استدعاء طريقة البدء. إذا تم تمرير المفوض ParametrizedThreadStart إلى المُنشئ ، فيجب أن يتم تمرير كائن إلى طريقة البدء. هذه العملية ضرورية لتمرير أي معلومات محلية إلى سلسلة الرسائل. يجب أن أشير إلى أن الأمر يتطلب الكثير من الموارد لإنشاء مؤشر ترابط وأن مؤشر الترابط نفسه كائن ثقيل - على الأقل لأنه يتطلب التفاعل مع واجهة برمجة تطبيقات OS ويتم تخصيص 1 ميغابايت من الذاكرة للمجموعة.
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. ستعرض هذه النافذة فقط معلومات حول مؤشرات الترابط عندما يتم تصحيح التطبيق في وضع الفاصل. هنا ، يمكنك عرض أسماء وأولويات كل مؤشر ترابط وتركيز وضع التصحيح على مؤشرات ترابط محددة. تتيح لك خاصية الأولوية لفئة مؤشر الترابط تعيين أولوية مؤشر الترابط. سيتم أخذ هذه الأولوية في الاعتبار عند قيام نظام التشغيل و CLR بتقسيم وقت المعالج بين مؤشرات الترابط.

مكتبة مهمة متوازية
ظهرت مكتبة Parallel Task (TPL) لأول مرة في .NET 4.0. حاليا ، هي الأداة الرئيسية للعمل مع عدم التزامن. سيتم اعتبار أي كود يستخدم الأساليب القديمة رمزًا قديمًا. الوحدة الرئيسية لـ TPL هي فئة
المهام من مساحة الاسم System.Threading.Tasks. تمثل المهام التجريد موضوع. باستخدام أحدث إصدار من C # ، حصلنا على طريقة أنيقة جديدة للعمل مع Tasks - مشغلي المزامنة / الانتظار. هذه تسمح لكتابة التعليمات البرمجية غير المتزامنة كما لو كانت بسيطة ومتزامنة ، بحيث يمكن لأولئك الذين ليسوا على دراية جيدة في نظرية الخيوط أن يكتبوا الآن تطبيقات لن تتعارض مع العمليات الطويلة. يعد استخدام async / await موضوعًا لمقال منفصل (أو حتى مقالات قليلة) ، لكنني سأحاول تحديد الأساسيات في بضع جمل:
- async هو معدِّل لطريقة تُرجع مهمة أو باطلة
- تنتظر هو عامل انتظار مهمة عدم حظر.
مرة أخرى: عادةً ما يسمح عامل التشغيل الذي ينتظر الانتظار (هناك استثناءات) للخيط الحالي بالانتقال ، وعندما يتم تنفيذ المهمة وسيكون الموضوع (في الواقع ، السياق ، لكننا سنعود إليه لاحقًا) مجانًا نتيجة لذلك ، سوف تستمر في تنفيذ الطريقة. في .NET ، يتم تطبيق هذه الآلية بنفس طريقة إرجاع العائد - يتم تحويل طريقة إلى فئة آلة حالة محدودة يمكن تنفيذها في أجزاء منفصلة استناداً إلى حالتها. إذا كان هذا الصوت ممتعًا ، فقد أوصي بكتابة أي جزء بسيط من التعليمات البرمجية استنادًا إلى المزامنة / الانتظار ، وتجميعه والبحث في التحويل البرمجي الخاص به بمساعدة 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(
يتم إنشاء مهمة باستخدام الخيارات التالية:
- LongRunning - يلمح هذا الخيار إلى حقيقة أن المهمة لا يمكن تنفيذها بسرعة. لذلك ، قد يكون من الأفضل إنشاء سلسلة رسائل منفصلة لهذه المهمة بدلاً من أخذ سلسلة موجودة من المجموعة لتقليل الأضرار التي تلحق بالمهام الأخرى.
- يمكن ترتيب AttachedToParent - المهام بشكل هرمي. إذا تم استخدام هذا الخيار ، فستكون المهمة في انتظار تنفيذ مهامها الفرعية بعد تنفيذها بنفسها.
- PreferFairness - يحدد هذا الخيار أنه يجب تنفيذ المهمة بشكل أفضل قبل المهام التي تم إنشاؤها لاحقًا. ومع ذلك ، فإن الأمر أكثر من اقتراح ، وبالتالي فإن النتيجة ليست مضمونة دائمًا.
المعلمة الثانية التي تم تمريرها إلى الأسلوب هي CancellationToken. لكي يتم إلغاء العملية بشكل صحيح بعد بدء تشغيلها بالفعل ، يجب أن يحتوي الرمز القابل للتنفيذ على اختبارات حالة CancellationToken. في حالة عدم وجود عمليات تحقق من هذا القبيل ، فإن أسلوب "إلغاء" الذي تم استدعاؤه على كائن CancellationTokenSource سيكون قادرًا على إيقاف تنفيذ المهمة قبل بدء المهمة بالفعل.
بالنسبة إلى المعلمة الأخيرة ، أرسلنا كائنًا من نوع TaskScheduler يسمى جدولة. يتم استخدام هذا الفصل مع فصوله الفرعية للتحكم في كيفية توزيع المهام بين الخيوط. بشكل افتراضي ، سيتم تنفيذ مهمة في سلسلة رسائل تم اختيارها عشوائيًا من المجموعة
يتم تطبيق عامل انتظار على المهمة التي تم تكوينها. هذا يعني أن الكود المكتوب بعده (إذا كان هناك مثل هذا الكود) سيتم تنفيذه في نفس السياق (غالبًا ، هذا يعني "على نفس الخيط") مثل الكود المكتوب من قبل.
تم تصنيف هذه الطريقة على أنها غير متزامنة ، مما يعني أنه يمكن استخدام عامل انتظار الانتظار ، لكن رمز الاتصال لن يكون قادرًا على انتظار التنفيذ. إذا كانت هناك حاجة إلى هذا الاحتمال ، فيجب أن تُرجع الطريقة مهمة. يمكن ملاحظة الأساليب التي تُسمى "باطلة غير متزامنة" في كثير من الأحيان: فهي عادة ما تكون مناولي الأحداث أو غيرها من الطرق التي تعمل تحت مبدأ النار وتنسى. إذا كان من الضروري الانتظار حتى يتم الانتهاء من التنفيذ وإرجاع النتيجة ، فعليك استخدام "المهمة".
بالنسبة للمهام التي تُرجع أسلوب StartNew ، يمكننا استدعاء ConfigureAwait باستخدام المعلمة false - ثم ، سوف يستمر التنفيذ بعد الانتظار في سياق عشوائي بدلاً من سياق تم التقاطه. يجب أن يتم ذلك دائمًا إذا لم تتطلب الشفرة المكتوبة بعد الانتظار سياق تنفيذ محددًا. هذه أيضًا توصية من MS عندما يتعلق الأمر بكتابة التعليمات البرمجية المقدمة كمكتبة.
لنلقِ نظرة على كيف يمكننا انتظار إنجاز المهمة. أدناه ، يمكنك أن ترى مثالاً للشفرة مع تعليقات تدل على أنه يتم تنفيذ الانتظار بطريقة جيدة أو سيئة نسبيًا.
public static async void AnotherMethod() { int result = await AsyncMethod();
في المثال الأول ، ننتظر تنفيذ المهمة دون حظر سلسلة عمليات الاتصال ، لذلك سنعود إلى معالجة النتيجة عندما تكون جاهزة. قبل حدوث ذلك ، يتم ترك مؤشر الترابط المتصل بمفرده.
في المحاولة الثانية ، نقوم بحظر مؤشر الترابط المتصل حتى يتم احتساب نتيجة الطريقة. هذا مقاربة سيئة لسببين. بادئ ذي بدء ، نحن نهدر خيطًا - مورداً قيماً للغاية - في انتظار بسيط. بالإضافة إلى ذلك ، إذا كانت الطريقة التي نتصل بها تحتوي على انتظار بينما المقصود من سياق التزامن العودة إلى سلسلة الرسائل بعد الانتظار ، سنصل إلى طريق مسدود. يحدث هذا لأن مؤشر ترابط الاستدعاء ينتظر نتيجة طريقة غير متزامنة ، وستحاول الطريقة غير المتزامنة نفسها دون جدوى متابعة تنفيذها في مؤشر ترابط الاستدعاء.
عيب آخر لهذا النهج هو زيادة تعقيد معالجة الأخطاء. في الواقع يمكن معالجة الأخطاء بسهولة في كود غير متزامن إذا تم استخدام async / انتظار - العملية في هذه الحالة مماثلة لتلك الموجودة في التعليمات البرمجية المتزامنة. ومع ذلك ، عند تطبيق انتظار متزامن على مهمة ، يتم التفاف الاستثناء الأولي في AggregateException. بمعنى آخر ، لمعالجة الاستثناء ، سنحتاج إلى استكشاف نوع InnerException وكتابة سلسلة if يدويًا في كتلة catch أو ، بدلاً من ذلك ، استخدم catch عندما تكون البنية بدلاً من السلسلة المعتادة من كتل catch.
تم وصف المثالين الأخيرين أيضًا بأنه سيئ نسبيًا لنفس الأسباب وكلاهما يحتوي على نفس المشكلات.
تعتبر طرقا WhenAny و WhenAll مفيدة للغاية عندما يتعلق الأمر بانتظار مجموعة من المهام - تقوم بلف هذه المهام في واحدة ، وسيتم تنفيذها إما عند بدء مهمة واحدة من المجموعة أو عند تنفيذ كل هذه المهام بنجاح.
وقف المواضيع
لأسباب مختلفة ، قد تكون هناك حاجة لإيقاف مؤشر ترابط بعد بدء تشغيله. هناك عدة طرق للقيام بذلك. فئة مؤشر الترابط له طريقتان مع أسماء مناسبة -
إحباط و
المقاطعة . أشجع بشدة استخدام أول واحد لأنه بعد أن يتم استدعاؤه ، سيكون هناك
ThreadAbortedException يتم طرحه في أي لحظة عشوائية أثناء معالجة أي تعليمات تم اختيارها تعسفيًا. أنت لا تتوقع مواجهة مثل هذا الاستثناء عند زيادة عدد صحيح صحيح ، أليس كذلك؟ حسنًا ، عند استخدام طريقة إحباط ، يصبح هذا احتمالًا حقيقيًا. إذا احتجت إلى رفض قدرة CLR على إنشاء مثل هذه الاستثناءات في جزء معين من التعليمات البرمجية ، فيمكنك لفها في سلسلة
الترابط. مكالمات
BeginCriticalRegion و
Thread.EndCriticalRegion . أي رمز مكتوب في كتلة أخيرا يتم التفاف في هذه المكالمات. هذا هو السبب في أنه يمكنك العثور على الكتل ذات المحاولة الفارغة والأخرى غير الفارغة أخيرًا في أعماق كود الإطار. تكره Microsoft هذه الطريقة إلى حد عدم تضمينها في .NET الأساسية.
تعمل طريقة
المقاطعة بطريقة أكثر قابلية للتنبؤ بها. يمكن مقاطعة مؤشر ترابط مع
ThreadInterruptedException فقط عندما يكون مؤشر الترابط في وضع الانتظار. ينتقل إلى هذه الحالة عند تعليقه أثناء انتظار WaitHandle ، يتم استدعاء قفل أو بعد Thread.Sleep.
كل من هذه الطرق لها عيب عدم القدرة على التنبؤ. للهروب من هذه المشكلة ، يجب علينا استخدام بنية
CancellationToken وفئة
CancellationTokenSource . الفكرة العامة هي: يتم إنشاء مثيل لفئة CancellationTokenSource ، ويمكن فقط لأولئك الذين يمتلكونها إيقاف العملية عن طريق استدعاء الأسلوب
Cancel . يتم إلغاء CancellationToken فقط إلى العملية. لا يمكن لمالكي CancellationToken إلغاء العملية بأنفسهم - يمكنهم فقط التحقق مما إذا كانت العملية قد تم إلغاؤها. يمكن تحقيق ذلك باستخدام خاصية منطقية
IsCancellationRequested وطريقة
ThrowIfCancelRequested . آخر واحد سينشئ
TaskCancelledException إذا تم استدعاء الأسلوب "إلغاء" على مثيل CancellationTokenSource الذي أنشأ CancellationToken. هذه هي الطريقة التي أوصي بها. تكمن الميزة في الطرق الموضحة مسبقًا في أنها توفر تحكمًا تامًا في حالات الاستثناء الدقيقة التي يمكن فيها إلغاء العملية.
ستكون الطريقة الأكثر وحشية لإيقاف مؤشر ترابط استدعاء دالة Win32 API تسمى TerminateThread. بعد استدعاء هذه الوظيفة ، يمكن أن يكون سلوك CLR غير متوقع تمامًا. في
MSDN ، يتم كتابة ما يلي حول هذه الوظيفة:
"TerminateThread هي وظيفة خطيرة يجب استخدامها فقط في الحالات القصوى. "تحويل واجهة برمجة تطبيقات وراثي إلى مهمة تعتمد على المهام باستخدام FromAsync
إذا كنت محظوظًا بما فيه الكفاية للعمل في مشروع بدأ بعد طرح المهام (وعندما لم تعد تحرض على الرعب الوجودي في معظم المطورين) ، فلن تضطر إلى التعامل مع واجهات برمجة التطبيقات القديمة - كلاهما الطرف الثالث وتلك التي كد فريقك في الماضي. لحسن الحظ ، قام فريق تطوير .NET Framework بتسهيل الأمر بالنسبة لنا - ولكن هذا كان يمكن أن يكون عناية ذاتية ، على حد علمنا. في أي حال ، فإن .NET لديه بعض الأدوات التي تساعد على جعل الكود المكتوب مع العلامات القديمة القديمة غير متزامن في الاعتبار بشكل سلس. أحد هذه الأساليب هو TaskFactory يسمى FromAsync. في المثال أدناه ، أقوم بالالتفاف على الأساليب غير المتزامنة القديمة لفئة WebRequest في مهمة باستخدام FromAsync.
object state = null; WebRequest wr = WebRequest.CreateHttp("http://github.com"); await Task.Factory.FromAsync( wr.BeginGetResponse, we.EndGetResponse );
إنه مجرد مثال ، وربما لن تفعل شيئًا من هذا القبيل بأنواع مدمجة. ومع ذلك ، تعج المشاريع القديمة بأساليب BeginDoSomething التي تُرجع أساليب IAsyncResult و EndDoSomething التي تستقبلها.تحويل واجهة برمجة تطبيقات وراثي إلى مهمة تعتمد على المهام باستخدام TaskCompletionSource
هناك أداة أخرى تستحق الاستكشاف وهي فئة
TaskCompletionSource . في وظيفتها والغرض منها ومبدأ العملية ، تشبه طريقة RegisterWaitForSingleObject من فئة ThreadPool التي ذكرتها سابقًا. تتيح لنا هذه الفئة التفاف واجهات برمجة التطبيقات القديمة غير المتزامنة بسهولة في المهام.
قد ترغب في القول إنني أخبرت بالفعل عن الأسلوب FromAsync من فئة TaskFactory التي خدمت هذه الأغراض. هنا ، يجب أن نتذكر التاريخ الكامل للنماذج غير المتزامنة التي قدمتها Microsoft في السنوات الـ 15 الماضية: قبل أنماط البرمجة غير المتزامنة القائمة على المهام (TAP) ، كانت هناك أنماط برمجة غير متزامنة (APP). كانت التطبيقات كلها تدور حول Begin DoSomething تقوم بإرجاع IAsyncResult وطريقة End DoSomething التي تقبلها - وطريقة FromAsync مثالية لتراث هذه السنوات. ومع ذلك ، بمرور الوقت ، تم استبدال ذلك بأنماط غير متزامنة تستند إلى الأحداث (EAP) والتي حددت أن الحدث يسمى عند تنفيذ عملية غير متزامنة بنجاح.TaskCompletionSource مثالية للالتفاف على واجهات برمجة التطبيقات القديمة المبنية حول نموذج الحدث في المهام. هذه هي الطريقة التي تعمل بها: تحتوي كائنات هذه الفئة على خاصية عامة تسمى Task ، يمكن التحكم في حالتها بطرق مختلفة من فئة TaskCompletionSource (SetResult ، SetException وما إلى ذلك). في الأماكن التي تم فيها تطبيق عامل الانتظار على هذه المهمة ، سيتم تنفيذه أو تعطله مع استثناء وفقًا للطريقة المطبقة على TaskCompletionSource. لفهمها بشكل أفضل ، دعنا ننظر إلى مثال التعليمة البرمجية هذا. هنا ، يتم التفاف بعض واجهات برمجة التطبيقات القديمة من عصر EAP في مهمة بمساعدة 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 يمكن أن تفعل أكثر من مجرد التفاف واجهات برمجة التطبيقات القديمة. تفتح هذه الفئة إمكانية مثيرة للاهتمام لتصميم واجهات برمجة التطبيقات المختلفة بناءً على المهام التي لا تشغل مؤشرات الترابط. الخيط ، كما نتذكر ، هو مورد مكلف يقتصر معظمه على ذاكرة الوصول العشوائي يمكننا الوصول بسهولة إلى هذا الحد عند تطوير تطبيق ويب قوي مع منطق أعمال معقد. دعونا نلقي نظرة على القدرات التي ذكرتها في العمل من خلال تنفيذ خدعة أنيقة معروفة باسم Long Polling.
باختصار ، هذه هي الطريقة التي يعمل بها الاقتراع الطويل:تحتاج إلى الحصول على بعض المعلومات من واجهة برمجة التطبيقات حول الأحداث التي تحدث من جانبها ، لكن واجهة برمجة التطبيقات ، لسبب ما ، لا يمكنها سوى إعادة الحالة بدلاً من إخبارك بالحدث. مثال على ذلك هو أي واجهة برمجة تطبيقات مبنية على HTTP قبل ظهور WebSocket أو في ظروف لا يمكن فيها استخدام هذه التقنية. يمكن للعميل أن يطلب خادم HTTP. خادم HTTP ، من ناحية أخرى ، لا يمكن بدء الاتصال مع العميل بنفسه. سيكون الحل الأبسط هو طلب الخادم بشكل دوري باستخدام جهاز ضبط الوقت ، ولكن هذا سيخلق حملًا إضافيًا للخادم وتأخيرًا عامًا يساوي تقريبًا TimerInterval / 2. لتجاوز هذا ، تم اختراع Long Polling. يستلزم تأخير استجابة الخادم حتى انتهاء المهلة أو حدوث حدث. في حالة حدوث حدث ، سيتم التعامل معه ؛ إن لم يكن - سيتم إرسال الطلب مرة أخرى. while(!eventOccures && !timeoutExceeded) { CheckTimout(); CheckEvent(); Thread.Sleep(1); }
ومع ذلك ، ستنخفض فعالية هذا الحل بشكل جذري في حالة زيادة عدد العملاء الذين ينتظرون الحدث - يحتل كل عميل ينتظر سلسلة كاملة. أيضًا ، حصلنا على تأخير إضافي قدره 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); } }
يرجى الانتباه إلى أن هذه الشفرة ليست سوى مثال ، وليست جاهزة للإنتاج بأي حال من الأحوال. لاستخدامها في الحالات الحقيقية ، نحتاج على الأقل إلى إضافة طريقة للتعامل مع المواقف التي يتم فيها تلقي رسالة عند عدم انتظار أي شيء: في هذه الحالة ، يجب أن تُرجع طريقة AcceptMessageAsync مهمة منتهية بالفعل. إذا كانت هذه الحالة هي الأكثر شيوعًا ، فيمكننا التفكير في استخدام 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); }
مع الرغبة في تحسينه قليلاً واهتمامًا بما ستنشئه روسلين عند تجميع هذا الرمز ، يمكننا إعادة كتابة الطريقة مثل هذا:
public Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return Task.FromResult(val); return RequestById(id); }
ومع ذلك ، فإن أفضل حل في هذه الحالة هو تحسين المسار السريع - على وجه التحديد ، الحصول على قيم القاموس بدون تخصيصات غير ضرورية وبدون تحميل على 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 # ، سوف تتصرف ValueTask تمامًا مثل المهمة المعتادة.
TaskScheduler: السيطرة على استراتيجيات تنفيذ المهمة
واجهة برمجة التطبيقات التالية التي أود التحدث عنها هي فئة
TaskScheduler وتلك المشتقة منها. لقد ذكرت بالفعل أن TPL توفر القدرة على التحكم في كيفية توزيع المهام بالضبط بين الخيوط. يتم تعريف هذه الاستراتيجيات في الفئات الموروثة من TaskScheduler. يمكن العثور على أي استراتيجية نحتاجها تقريبًا في مكتبة
ParallelExtensionsExtras . تم تطوير هذه المكتبة بواسطة Microsoft ، ولكنها ليست جزءًا من .NET - بل يتم توزيعها كحزمة Nuget. دعونا نلقي نظرة على بعض الاستراتيجيات:
- CurrentThreadTaskScheduler - ينفذ المهام على الموضوع الحالي
- LimitedConcurrencyLevelTaskScheduler - يحد من عدد المهام المنفذة في وقت واحد باستخدام المعلمة N التي يقبلها في المُنشئ
- OrderedTaskScheduler - يتم تعريفه على أنه LimitedConcurrencyLevelTaskScheduler (1) ، لذا سيتم تنفيذ المهام بالتسلسل.
- WorkStealingTaskScheduler - تنفذ نهج سرقة العمل لتنفيذ المهمة. في الأساس ، يمكن أن ينظر إليه باعتباره ThreadPool منفصلة. يساعد هذا في أن تكون ThreadPool هي فئة ثابتة في .NET - إذا كانت محملة بشكل زائد أو تستخدم بشكل غير صحيح في جزء واحد من التطبيق ، فقد تحدث آثار جانبية غير سارة في مكان مختلف. قد يكون من الصعب تحديد الأسباب الحقيقية لمثل هذه العيوب ، لذلك قد تحتاج إلى استخدام WorkStealingTaskSchedulers منفصلة في تلك الأجزاء من التطبيق حيث يمكن أن يكون استخدام ThreadPool عدوانيًا ولا يمكن التنبؤ به.
- QueuedTaskScheduler - يسمح بتنفيذ المهام على أساس قائمة انتظار ذات أولوية
- يقوم ThreadPerTaskScheduler - بإنشاء مؤشر ترابط منفصل لكل مهمة يتم تنفيذها عليها. قد يكون ذلك مفيدًا للمهام التي لا يمكن تقدير وقت تنفيذها.
يوجد
مقال جيد جدًا حول TaskSchedulers على مدونة Microsoft ، لذلك لا تتردد في التحقق من ذلك.
في Visual Studio ، يوجد إطار مهام يمكنه المساعدة في تصحيح كل ما يتعلق بالمهام. في هذا الإطار ، يمكنك رؤية حالة المهمة والانتقال إلى سطر التعليمات البرمجية الذي يتم تنفيذه حاليًا.

PLinq والطبقة الموازية
بصرف النظر عن المهام وجميع الأشياء المتعلقة بها ، هناك أداتان إضافيتان في .NET يمكن أن
نجدهما ممتعين -
PLinq (Linq2Parallel) وفئة
Parallel . أول واحد يعد بالتنفيذ الموازي لجميع عمليات Linq على جميع المواضيع. يمكن تكوين عدد مؤشرات الترابط بواسطة أسلوب ملحق WithDegreeOfParallelism. لسوء الحظ ، في معظم الحالات ، لن يكون لدى PLinq في الوضع الافتراضي معلومات كافية حول مصدر البيانات لتوفير زيادة كبيرة في السرعة. من ناحية أخرى ، فإن تكلفة المحاولة منخفضة للغاية: تحتاج فقط إلى استدعاء
AsParallel قبل سلسلة أساليب Linq وإجراء اختبارات الأداء. علاوة على ذلك ، يمكنك تمرير معلومات إضافية حول طبيعة مصدر البيانات الخاص بك إلى PLinq باستخدام آلية الأقسام. يمكنك العثور على مزيد من المعلومات
هنا وهنا .
توفر الفئة الساكنة المتوازية طرقًا لتعداد المجموعات بالتوازي عبر Foreach ، وإدارة For دورة وتنفيذ العديد من المفوضين بالتوازي مع Invoke. سيتم إيقاف تنفيذ سلسلة العمليات الحالية حتى يتم حساب النتائج. يمكنك تكوين عدد مؤشرات الترابط عن طريق تمرير ParallelOptions كوسيطة أخيرة. يمكن أيضًا تعيين TaskScheduler و CancellationToken بمساعدة الخيارات.
ملخص
عندما بدأت في كتابة هذا المقال بناءً على رسالتي وعلى المعرفة التي اكتسبتها أثناء العمل بعدها ، لم أكن أعتقد أنه سيكون هناك الكثير من هذه المعلومات. الآن ، بعد أن أخبرني محرر النص بعبارة أنني كتبت حوالي 15 صفحة ، أود استخلاص وسيط. سنبحث في التقنيات الأخرى ، واجهات برمجة التطبيقات ، والأدوات المرئية والمخاطر الخفية في المقالة التالية.
الاستنتاجات:- لاستخدام موارد أجهزة الكمبيوتر الحديثة بفعالية ، ستحتاج إلى معرفة الأدوات اللازمة للتعامل مع مؤشرات الترابط وعدم التزامن والتوازي.
- هناك العديد من الأدوات مثل هذا في .NET
- لم يتم إنشاء كل هذه العناصر في نفس الوقت ، لذا فقد تواجه غالبًا بعض التعليمات البرمجية القديمة - ولكن هناك طرق لتحويل واجهات برمجة التطبيقات القديمة مع القليل من الجهد.
- في .NET ، يتم استخدام فئات مؤشر الترابط و ThreadPool للعمل مع مؤشرات الترابط
- أسلوب Thread.Abort و Thread.Interrupt ، جنبا إلى جنب مع وظيفة Win32 API TerminateThread ، خطيرة ولا ينصح للاستخدام. بدلاً من ذلك ، من الأفضل استخدام CancellationTokens
- الخيوط مورد ثمين وعددهم محدود. يجب عليك تجنب الحالات التي يتم فيها احتلال سلاسل الرسائل عن طريق انتظار الأحداث. يمكن أن تساعد فئة TaskCompletionSource في تحقيق ذلك.
- المهام هي الأداة الأقوى والأكثر قوة التي يستخدمها .NET للعمل مع التوازي والتزامن.
- يقوم العاملون المتزامنون / المنتظرون في C # بتطبيق مفهوم الانتظار بدون حظر
- يمكنك التحكم في كيفية توزيع المهام بين سلاسل الرسائل بمساعدة الفئات المشتقة من TaskScheduler
- يمكن استخدام بنية ValueTask لتحسين المسارات الساخنة وحركة الذاكرة
- توفر الإطارات "المهام" و "مؤشرات الترابط" في Visual Studio الكثير من المعلومات المفيدة لتصحيح التعليمات البرمجية متعددة مؤشرات الترابط أو غير متزامن
- PLinq هي أداة رائعة ، ولكنها قد لا تحتوي على جميع المعلومات المطلوبة حول مصدر البيانات - والتي لا يزال من الممكن إصلاحها باستخدام آلية التقسيم
أن تستمر ...