نسخة نصية من تقرير "الفاعلون مقابل CSP مقابل المهام ..." مع C ++ CoreHard خريف 2018

في أوائل تشرين الثاني / نوفمبر ، استضافت مينسك مؤتمر C ++ التالي مؤتمر C ++ CoreHard لخريف 2018. وقدمت تقرير القبطان "Actors vs CSP vs Tasks ..." ، الذي تحدث عن كيف يمكن للتطبيقات ذات المستوى الأعلى من "الظهور في C ++" "برامج متعددة تنافسية". تحت نسخة مقطوعة من هذا التقرير ، تحولت إلى مقال. ممشط ، مشذب في أماكن ، مكمل في أماكن.

أود أن أغتنم هذه الفرصة لأشكر مجتمع CoreHard على تنظيم المؤتمر الكبير القادم في مينسك وعلى فرصة التحدث. وأيضا للنشر الفوري لتقارير الفيديو للتقارير على موقع يوتيوب .

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

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

multithreading عارية هو الشر!


عليك أن تبدأ بالتفاهات المتكررة ، والتي ، مع ذلك ، لا تزال ذات صلة:
برمجة C ++ متعددة مؤشرات الترابط من خلال الخيوط العارية ، ومتغيرات التبادل والحالة هي العرق والألم والدم .

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

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

تناول الأشخاص لغة C / C ++ أثناء العمل على الإصدار الأول من خادمهم وأعادوا كتابة الخادم بلغة أخرى.

عرض ممتاز لكيفية رفض المطورين استخدام C ++ في العالم الواقعي ، خارج مجتمع C ++ المريح ، حتى عندما يكون استخدام C ++ مناسبًا ومبررًا.

لكن لماذا؟


لكن لماذا ، إذا قيل مرارًا وتكرارًا أن "تعدد مؤشرات الترابط" في لغة C ++ أمر شرير ، يستمر الناس في استخدامه بمثابرة جديرة بتطبيق أفضل؟ ما هو اللوم:

  • جهل؟
  • الكسل؟
  • متلازمة NIH؟

بعد كل شيء ، ليس هناك سوى نهج واحد تم اختباره عبر الزمن والعديد من المشاريع. على وجه الخصوص:

  • الجهات الفاعلة
  • توصيل العمليات المتسلسلة (CSP)
  • المهام (غير متزامنة ، وعود ، مستقبلية ، ...)
  • تدفقات البيانات
  • البرمجة التفاعلية
  • ...

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

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

تحدي التجارب


مطلوب لتنفيذ خادم HTTP الذي:

  • قبول الطلب (معرف الصورة ، معرف المستخدم) ؛
  • يعطي صورة مع "علامات مائية" فريدة لهذا المستخدم.

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

المهمة مجردة ، وقد صيغت خصيصًا لهذا التقرير تحت تأثير مشروعنا التجريبي الروبيان (لقد تحدثنا بالفعل عن ذلك: رقم 1 ، رقم 2 ، رقم 3 ).

سيعمل خادم HTTP هذا على النحو التالي:

بعد تلقي طلب من العميل ، ننتقل إلى خدمتين خارجيتين:

  • الأول يعيد لنا معلومات المستخدم. بما في ذلك من هناك نحصل على صورة "العلامات المائية" ؛
  • الثانية تعيد لنا الصورة الأصلية

تعمل هاتان الخدمتان بشكل مستقل ويمكننا الوصول إليهما في وقت واحد.

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

لكن نموذج one-request = نموذج سير عمل واحد مكلف للغاية ولا يتطور بشكل جيد. نحن لا نحتاج هذا.

حتى لو اقتربنا من عدد تدفقات العمل بشكل مُهدر ، ما زلنا بحاجة إلى عدد صغير منهم:

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

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

بعض التنازلات الهامة


قبل الانتقال إلى القصة الرئيسية وأمثلة رمز التحليل ، يجب عمل بعض الملاحظات.

أولاً ، لا ترتبط جميع الأمثلة التالية بأي إطار أو مكتبة معينة. أي تطابقات في أسماء مكالمات API تكون عشوائية وغير مقصودة.

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

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

النهج رقم 1: الفاعلون


نموذج الممثلين باختصار


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

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

يعمل الممثلون على مبادئ بسيطة للغاية:

  • الفاعل هو كيان له سلوك ؛
  • الجهات الفاعلة الرد على الرسائل الواردة ؛
  • بعد استلام الرسالة ، يمكن للممثل:
    • إرسال بعض الرسائل (النهائية) إلى جهات فاعلة أخرى ؛
    • إنشاء عدد (نهائي) من الجهات الفاعلة الجديدة ؛
    • تحديد سلوك جديد لمعالجة الرسائل اللاحقة.

داخل التطبيق ، يمكن تنفيذ الممثلين بطرق مختلفة:

  • يمكن تمثيل كل ممثل كتيار منفصل لنظام التشغيل (يحدث هذا ، على سبيل المثال ، في مكتبة C :: Just :: Thread Pro Actor Edition) ؛
  • يمكن تمثيل كل ممثل باعتباره روتين مكدس ؛
  • يمكن تمثيل كل ممثل ككائن يقوم فيه شخص ما باستدعاء طرق رد الاتصال.

في قرارنا ، سوف نستخدم الجهات الفاعلة في شكل كائنات مع الاسترجاعات ، ونترك coroutines لمنهج CSP.

مخطط القرار على أساس نموذج الممثلين


بناءً على الجهات الفاعلة ، سيبدو المخطط العام لحل مشكلتنا كما يلي:

سيكون لدينا ممثلين تم إنشاؤهم في بداية خادم HTTP وهم موجودون طوال الوقت أثناء عمل خادم HTTP. هؤلاء هم الجهات الفاعلة مثل: HttpSrv و UserChecker و ImageDownloader و ImageMixer.

عند استلام طلب HTTP جديد وارد ، نقوم بإنشاء مثيل جديد لممثل RequestHandler ، والذي سيتم تدميره بعد إصدار رد على طلب HTTP الوارد.

RequestHandler رمز الممثل


يمكن أن يبدو تنفيذ عامل request_handler ، الذي ينسق معالجة طلب HTTP الوارد كما يلي:
class request_handler final : public some_basic_type { const execution_context context_; const request request_; optional<user_info> user_info_; optional<image_loaded> image_; void on_start(); void on_user_info(user_info info); void on_image_loaded(image_loaded image); void on_mixed_image(mixed_image image); void send_mix_images_request(); ... //     . }; void request_handler::on_start() { send(context_.user_checker(), check_user{request_.user_id(), self()}); send(context_.image_downloader(), download_image{request_.image_id(), self()}); } void request_handler::on_user_info(user_info info) { user_info_ = std::move(info); if(image_) send_mix_images_request(); } void request_handler::on_image_loaded(image_loaded image) { image_ = std::move(image); if(user_info_) send_mix_images_request(); } void request_handler::send_mix_images_request() { send(context_.image_mixer(), mix_images{user_info->watermark_image(), *image_, self()}); } void request_handler::on_mixed_image(mixed_image image) { send(context_.http_srv(), reply{..., std::move(image), ...}); } 

دعونا تحليل هذا الرمز.

لدينا فئة في السمات التي نخزنها أو سنقوم بتخزين ما نحتاجه لمعالجة الطلب. أيضا في هذه الفئة هناك مجموعة من الاسترجاعات التي سيتم استدعاؤها في وقت أو آخر.

أولاً ، عندما يتم إنشاء ممثل ، يتم استدعاء رد الاتصال on_start (). نرسل فيه رسالتين إلى ممثلين آخرين. أولاً ، هذه رسالة check_user للتحقق من معرف العميل. ثانيًا ، هذه رسالة download_image لتنزيل الصورة الأصلية.

في كل رسالة من الرسائل المرسلة ، نقوم بتمرير رابط لأنفسنا (تؤدي طريقة استدعاء الذات () إلى إرجاع رابط إلى الفاعل الذي تم استدعاء الذات () له)). هذا ضروري حتى يتمكن ممثلنا من إرسال رسالة استجابة. إذا لم نرسل رابطًا إلى ممثلنا ، على سبيل المثال ، في رسالة check_user ، فلن يعرف ممثل UserChecker إلى من سيتم إرسال معلومات المستخدم إليه.

عندما يتم إرسال رسالة user_info تتضمن معلومات المستخدم إلينا استجابة ، يتم استدعاء رد الاتصال on_user_info (). وعندما يتم إرسال رسالة image_loaded إلينا ، يتم استدعاء رد الاتصال on_image_loaded () على ممثلنا. والآن داخل هاتين الاستدعاءات نرى ميزة متأصلة في نموذج الممثلين: لا نعرف بالضبط الترتيب الذي سنتلقى فيه رسائل الاستجابة. لذلك ، يجب أن نكتب رمزنا بحيث لا يعتمد على ترتيب وصول الرسائل. لذلك ، في كل من المعالجات ، نقوم أولاً بتخزين المعلومات المستلمة في السمة المقابلة ، ثم نتحقق مما إذا كنا قد جمعنا بالفعل جميع المعلومات التي نحتاجها؟ إذا كان الأمر كذلك ، فيمكننا المضي قدمًا. إذا لم يكن كذلك ، فإننا سننتظر أكثر.

هذا هو سبب وجود ons في on_user_info () و on_image_loaded () إذا تم استدعاء send_mix_images_request ().

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

لذلك ، إذا تم تلقي جميع المعلومات التي نحتاجها من UserChecker و ImageDownloader ، فسيتم استدعاء طريقة send_mix_images_request () ، حيث يتم إرسال رسالة mix_images إلى ممثل ImageMixer. يتم استدعاء رد الاتصال on_mixed_image () عندما نتلقى رسالة استجابة مع الصورة الناتجة. هنا نرسل هذه الصورة إلى ممثل HttpSrv وننتظر حتى يشكل HttpSrv استجابة HTTP ويدمر RequestHandler الذي أصبح غير ضروري (على الرغم من أنه ، من حيث المبدأ ، لا يوجد شيء يمنع الممثل RequestHandler من التدمير الذاتي في رد الاتصال on_mixed_image ()).

هذا كل شئ.

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

الميزات المتأصلة في الممثلين


الآن يمكننا أن نقول بضع كلمات حول ميزات نموذج الممثلين.

المفاعلات


كقاعدة ، يستجيب الممثلون فقط للرسائل الواردة. هناك رسائل - يعالجها الممثل. لا رسائل - الفاعل لا يفعل شيئا.

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

يتم تحميل الممثلين بشكل زائد


بالنسبة للممثلين ، يمكننا بسهولة أن نجعل الممثل - المنتج يولد رسائل للمستهلك - الفاعل بوتيرة أسرع بكثير من قدرة الممثل - المستهلك على معالجتها.

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

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

لذلك عند العمل مع الجهات الفاعلة ، يجب إيلاء اهتمام جدي لمشكلة التحميل الزائد.

العديد من الممثلين ليسوا دائما الحل.


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

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

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

إلى أين ننظر ، وماذا تأخذ؟


إذا أراد شخص ما محاولة العمل مع الممثلين في C ++ ، فلا فائدة من بناء دراجاتك الخاصة ، فهناك العديد من الحلول الجاهزة ، على وجه الخصوص:


هذه الخيارات الثلاثة حية ومتطورة ومتعددة المنصات وموثقة. يمكنك أيضًا تجربتها مجانًا. بالإضافة إلى العديد من الخيارات بدرجات متفاوتة من [لا] نضارة يمكن العثور عليها في القائمة على ويكيبيديا .

تم تصميم SObjectizer و CAF للاستخدام في المهام عالية المستوى إلى حد ما حيث يمكن تطبيق الاستثناءات والذاكرة الديناميكية. وقد يكون إطار QP / C ++ محل اهتمام المشاركين في التنمية المدمجة ، مثل تحت هذا المكان هو "سجن".

النهج رقم 2: CSP (توصيل العمليات المتسلسلة)


CSP على الأصابع وبدون ماتان


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

فقط هذه الكيانات في نموذج CSP تسمى "العمليات".

العمليات في CSP خفيفة الوزن ، دون أي موازاة لعملهم في الداخل. إذا كنا بحاجة إلى موازاة شيء ما ، فإننا ببساطة نبدأ في العديد من عمليات CSP ، التي لم يعد هناك أي موازٍ فيها.

تتفاعل عمليات CSP مع بعضها البعض عبر الرسائل غير المتزامنة ، ولكن لا يتم إرسال الرسائل إلى صناديق البريد ، كما هو الحال في نموذج الممثلين ، ولكن إلى القنوات. يمكن اعتبار القنوات بمثابة قوائم انتظار للرسائل ، عادة ما تكون ذات حجم ثابت.

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

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

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

في حين أن عملية CSP نفسها يجب أن تختار لحظة مكالمة القراءة / الاستلام ، فإن عملية CSP يجب أن تحدد الرسالة التي تلقتها وتعالج الرسالة المستخرجة.

داخل تطبيقنا "الكبير" ، يمكن تنفيذ عمليات CSP بطرق مختلفة:

  • يمكن تنفيذ عملية CSP-shny كنظام تشغيل مؤشر ترابط منفصل. اتضح حلاً باهظ الثمن ، ولكن مع تعدد المهام الوقائي.
  • يمكن تنفيذ عملية CSP بواسطة coroutine (coroutine المكدس ، والألياف ، والخيط الأخضر ، ...). إنها أرخص بكثير ، لكن تعدد المهام هو تعاونية فقط.

علاوة على ذلك ، نفترض أن عمليات CSP يتم تقديمها في شكل Coroutines المكدسة (على الرغم من أن الكود الموضح أدناه قد يتم تنفيذه بشكل جيد على سلاسل نظام التشغيل).

مخطط الحل القائم على CSP


يشبه مخطط الحل القائم على نموذج CSP إلى حد كبير مخططًا مشابهًا لنموذج الممثلين (وهذا ليس من قبيل الصدفة):

ستكون هناك أيضًا كيانات تبدأ في بداية خادم HTTP وتعمل طوال الوقت - هذه هي عمليات CSP HttpSrv و UserChecker و ImageDownloader و ImageMixer. لكل طلب وارد جديد ، سيتم إنشاء عملية RequestHandler CSP جديدة. تقوم هذه العملية بإرسال واستقبال الرسائل نفسها عند استخدام نموذج الممثلين.

RequestHandler CSP Process Code


قد يبدو هذا وكأنه رمز دالة تنفذ عملية RequestHandler CSP:
 void request_handler(const execution_context ctx, const request req) { auto user_info_ch = make_chain<user_info>(); auto image_loaded_ch = make_chain<image_loaded>(); ctx.user_checker_ch().write(check_user{req.user_id(), user_info_ch}); ctx.image_downloader_ch().write(download_image{req.image_id(), image_loaded_ch}); auto user = user_info_ch.read(); auto original_image = image_loaded_ch.read(); auto image_mix_ch = make_chain<mixed_image>(); ctx.image_mixer_ch().write( mix_image{user.watermark_image(), std::move(original_image), image_mix_ch}); auto result_image = image_mix_ch.read(); ctx.http_srv_ch().write(reply{..., std::move(result_image), ...}); } 

هنا كل شيء تافه ويكرر نفس النمط بانتظام:

  • أولاً ، ننشئ قناة لاستقبال رسائل الاستجابة. هذا ضروري لأن لا تحتوي عملية CSP على صندوق بريد افتراضي خاص بها ، مثل الممثلين. لذلك ، إذا كانت عملية CSP-shny تريد تلقي شيء ما ، فيجب أن تكون في حيرة من إنشاء قناة حيث سيتم كتابة هذا "شيء" ؛
  • ثم نرسل رسالتنا إلى عملية CSP الرئيسية. ونشير في هذه الرسالة إلى قناة رسالة الاستجابة ؛
  • ثم نقوم بإجراء عملية القراءة من القناة حيث يجب أن نرسل رسالة استجابة.

يظهر هذا بوضوح في مثال التواصل مع عملية ImageSPixer CSP:
 auto image_mix_ch = make_chain<mixed_image>(); //  . ctx.image_mixer_ch().write( //  . mix_image{..., image_mix_ch}); //     . auto result_image = image_mix_ch.read(); //  . 

ولكن بشكل منفصل ، يجدر التركيز على هذا الجزء:
  auto user = user_info_ch.read(); auto original_image = image_loaded_ch.read(); 

هنا نرى اختلافًا خطيرًا آخر عن نموذج الممثلين. في حالة CSP ، يمكننا تلقي رسائل الاستجابة بالترتيب الذي يناسبنا.

هل تريد الانتظار للحصول على معلومات المستخدم أولاً؟ حَسَنًا ، تَوَقَّفْ فِي الْقَراءةْ حَتَّى يَظْهِرْ user_info. إذا تم إرسال image_loaded إلينا بالفعل في هذا الوقت ، فسوف تنتظر ببساطة في قناتها حتى نقرأها.

هذا ، في الواقع ، كل ما يمكن أن يصاحب الرمز الموضح أعلاه. كان الرمز المعتمد على CSP أكثر إحكاما من نظيره القائم على الممثل. وهو ليس مفاجئا منذ ذلك الحين هنا لم يكن لدينا لوصف فصل منفصل مع أساليب رد الاتصال. وجزء من حالة عملية CSP - خجولة لدينا RequestHandler موجود ضمنيًا في شكل الحجج ctx و req.

ميزات CSP


تفاعلية واستباقية عمليات CSP


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

بعد مرور بعض الوقت ، تعبت عملية CSP للمصفوفة من الضرب ، وراجع مرة أخرى الرسائل الواردة. لا جديدة؟ حسنًا ، حسنًا ، دعنا نذهب لمضاعفة المصفوفات أكثر.

وهذه القدرة على عمليات CSP للقيام ببعض الأعمال حتى في غياب الرسائل الواردة تجعل نموذج CSP مختلفًا تمامًا عن نموذج الممثلين.

آليات حماية الزائد الأصلية


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

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

وبالتالي ، عند استخدام CSP ، يمكننا أن نقلق بشأن مشكلة الحمل الزائد أقل مما في حالة نموذج الممثلين. صحيح ، هناك مأزق هنا ، سنتحدث عنه بعد ذلك بقليل.

كيف يتم تنفيذ عمليات CSP


يجب أن نقرر كيف سيتم تنفيذ عمليات CSP الخاصة بنا.

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

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

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

لذلك ، تحتاج إلى التفكير بعناية في اختيار الطريقة لتنفيذ عمليات CSP-shnyh. لكل خيار من نقاط القوة والضعف الخاصة به.

العديد من العمليات ليست الحل دائمًا.


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

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

والحقيقة هي أنه على قنوات CSP-shnyh يمكنك بسهولة الحصول على تناظرية من الجمود. تحاول العملية "أ" كتابة رسالة إلى القناة الكاملة C1 والعملية "أ" متوقفة مؤقتًا. من القناة C1 ، يجب قراءة العملية B ، التي حاولت الكتابة إلى القناة C2 ، الممتلئة ، وبالتالي ، تم تعليق العملية B. ومن القناة C2 ، كانت العملية أ هي القراءة. هذا كل شيء ، وصلنا إلى طريق مسدود.

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

إلى أين ننظر ، وماذا تأخذ؟


إذا أراد شخص ما العمل مع CSP في C ++ ، فإن الخيار هنا ، للأسف ، ليس كبيرًا مثل الممثلين. حسنًا ، أو لا أعرف أين أبحث وكيف أنظر. في هذه الحالة ، آمل أن تشارك التعليقات روابط أخرى.

ولكن ، إذا أردنا استخدام CSP ، نحتاج أولاً إلى التطلع إلى Boost.Fiber . هناك الألياف (أي coroutines) ، والقنوات ، وحتى البدائية منخفضة المستوى مثل mutex ، condition_variable ، الحاجز. كل هذا يمكن أن يؤخذ ويستخدم.

إذا كنت راضيًا عن عمليات CSP في شكل خيوط ، فيمكنك إلقاء نظرة على SObjectizer . هناك أيضًا نظائر لقنوات CSP وتطبيقات معقدة متعددة الخيوط على SObjectizer يمكن كتابتها دون أي ممثلين على الإطلاق.

الفاعلون مقابل CSP


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

ومع ذلك ، هناك العديد من الاختلافات بين الجهات الفاعلة ومقدمي خدمات التشييد (CSP) التي يمكن أن تساعد في تحديد أين يكون كل من هذه النماذج مفيدًا أو غير مؤات.

القنوات مقابل صندوق البريد


لدى الممثل "قناة" واحدة لاستقبال الرسائل الواردة - هذا هو صندوق البريد الخاص به ، والذي يتم إنشاؤه تلقائيًا لكل ممثل. ويقوم الممثل باسترداد الرسائل من هناك بالتسلسل ، بالضبط بنفس الترتيب الذي كانت به الرسائل في صندوق البريد.

وهذا سؤال خطير للغاية. لنفترض أن هناك ثلاث رسائل في صندوق بريد الممثل: M1 و M2 و M3. الممثل مهتم حاليًا فقط بـ M3. M3 M1, M2. ?

selective receive Erlang stashing- Akka.

CSP- , . , CSP- : C1, C2 C3. CSP- C3. . C1 C2 , .


, , .

CSP- - . - .


(). , , , , CSP- .

C++ CSP


Go , CSP .

Go «CSP- » (aka goroutines), , (Go- select, , ), stdlib.

C++ stackful coroutines ( ). CSP C++ , , , … , Go.

№3: Tasks (async, future, wait_all, ...)


Task-based


Task-based , , -, ( task) - .

async. async -future, , .

, N N -future, - . , №1 №2, №3. №3, №4, №5 №6. .., ..

«» . , , .then() -future, wait_all(), wait_any().

« » , . ( ).

request_handler- Task-based


HTTP- task- :
 void handle_request(const execution_context & ctx, request req) { auto user_info_ft = async(ctx.http_client_ctx(), [req] { return retrieve_user_info(req.user_id()); }); auto original_image_ft = async(ctx.http_client_ctx(), [req] { return download_image(req.image_id()); }); when_all(user_info_ft, original_image_ft).then( [&ctx, req](tuple<future<user_info>, future<image_loaded>> data) { async(ctx.image_mixer_ctx(), [&ctx, req, d=std::move(data)] { return mix_image(get<0>(d).get().watermark_image(), get<1>(d).get()); }) .then([req](future<mixed_image> mixed) { async(ctx.http_srv_ctx(), [req, im=std::move(mixed)] { make_reply(...); }); }); }); } 

, .

, HTTP- . -future user_info_ft.

, HTTP- . -future original_image_ft.

. : when_all(user_info_ft, original_image_ft). -future , . « » ImageMixer-. , HTTP- , HTTP-.

. :

( ):

, , , :


Task-based



, — Task-. .

, callback hell. Node.js-. C++, Task-, callback hell.

خطأ في التعامل


— .

, async future , CSP. CSP A B , B , A:

  • ;
  • , std::variant .

future : future , .

, , . , №1 , future, №2. №2 future, . , , . , future, №3. , , , . إلخ.

, , .

Task- /


Task-based — - . , , 150 , 10 , , . 140 ? :)

— . , - 50- . , -, , ? , , :)


Task-based . :
  auto user_info_ft = async(ctx.http_client_ctx(), [req] { return retrieve_user_info(req.user_id()); }); auto original_image_ft = async(ctx.http_client_ctx(), [req] { return download_image(req.image_id()); }); 

HTTP- , . , , . , - .

Actors/CSP vs Tasks


, CSP- , Task-based . , Actors/CSP Task-.

.

CSP, . : , , .

أي CSP .

, , CSP- . ?

Task-based , . , , , , ..

أي Task- .

, Actors/CSP Tasks , . Actors/CSP . Tasks .

, Actor- , ImageMixer, . - ImageMixer Task-based .

, ?


Task- C++, C++20. .then() future, wait_all() wait_any. cppreference .

async++ . , , , - .

Microsoft PPL . , , .

Intel TBB. Task-based , , TBB — data flow . , , Intel TBB , data flow.


, , : " ++20. Coroutines TS ".

Task-based stackless coroutines ++20. , Task- CSP- .

- Task-based , .

الخلاصة


, , .

, , — , - - .

, , - lock-free lock-free . , :

  • actors
  • communicating sequential processes (CSP)
  • tasks (async, promises, futures, ...)
  • data flows
  • reactive programming
  • ...

, C++ . , , , , .

: , .

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


All Articles