مجرد إلقاء نظرة على SObjectizer إذا كنت تريد استخدام Actors أو CSP في مشروع C ++


بضع كلمات عن SObjectizer وتاريخها


SObjectizer هو إطار C ++ صغير إلى حد ما يبسط تطوير تطبيقات ذات مؤشرات ترابط متعددة. يسمح SObjectizer للمطور باستخدام الأساليب من نماذج العمليات الفاعلة ونشر الاشتراك والتواصل (CSP). إنه مشروع مفتوح المصدر يتم توزيعه بموجب ترخيص BSD-3-CLAUSE.


SObjectizer له تاريخ طويل. ولدت SObjectizer نفسها في عام 2002 كمشروع SObjectizer-4. لكنها كانت تستند إلى أفكار من SCADA Objectizer السابقة تم تطويرها بين عامي 1995 و 2000. تم طرح SObjectizer-4 في عام 2006 ، لكن تطوره توقف بعد ذلك بفترة قصيرة. تم بدء إصدار جديد من SObjectizer يحمل الاسم SObjectizer-5 في عام 2010 وكان مفتوح المصدر في عام 2013. ما زال تطور SObjectizer-5 قيد التقدم ودمج SObjectizer-5 العديد من الميزات الجديدة منذ عام 2013.


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


مكانة SObjectizer وأدوات مماثلة


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


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


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


تهدف المناهج مثل Actor Model أو CSP إلى التعامل مع الحوسبة المتزامنة. أمثلة جيدة على الاستخدام الممثلون في مجال الحوسبة المتزامنة هم مشروع InfineSQL و Yandex Message-Queue . كل من هذه المشاريع استخدام الجهات الفاعلة في الداخل.


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


تنصل


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


ما SObjectizer يمكن أن تعطي للمستخدم؟


لا شيء مشترك ومبادئ النسيان والنفاد خارج الصندوق


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


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


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


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


المرسلون


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


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


هناك ثمانية أنواع من المرسلين في SObjectizer-5.6.0 (ويمكن العثور على واحد آخر في مشروع رفيق so5extra): بدءًا من الموجهات البسيطة جدًا (one_thread أو thread_pool) إلى المتطورة (مثل adv_thread_pool أو prio_dedicated_threads :: one_per_prio). ويمكن للمستخدم كتابة المرسل الخاص به لظروف محددة.


أجهزة الحالة الهرمية مدمجة في الوظائف


الوكلاء (الممثلون) في SObjectizer هم آلات الحالة: يعتمد رد الفعل على رسالة واردة على الحالة الحالية للوكيل. يدعم SObjectizer معظم ميزات أجهزة الحالة التسلسلية (HSM): الحالات المتداخلة ، والتاريخ العميق والضحل للحالة ، والمعالجات on_enter / on_exit ، والحدود الزمنية للبقاء في ولاية. لا يتم دعم الحالات المتعامدة فقط في SObjectizer الآن (لم نر ضرورة لهذه الميزة في مشاريعنا ، ولم يطلب منا أحد إضافة دعم لهذه الميزة).


قنوات تشبه CSP خارج الصندوق


ليست هناك حاجة لاستخدام وكلاء SObjectizer (الملقب الجهات الفاعلة). يمكن تطوير التطبيق بالكامل فقط باستخدام كائنات std::thread و سُلائل SObjectizer (ويعرف أيضًا باسم CSP-channel) . في هذه الحالة ، سيكون تطوير التطبيق مع SObjectizer مشابهًا إلى حد ما للتطوير بلغة Go (بما في ذلك التناظرية الخاصة بـ Go select التي تتيح انتظار الرسائل من عدة قنوات).


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


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


فقط جزء من التطبيق يمكنه استخدام SObjectizer


ليست هناك حاجة لاستخدام SObjectizer في كل جزء في التطبيق. يمكن تطوير جزء فقط من تطبيق ما باستخدام SObjectizer. لذلك إذا كنت تستخدم Qt أو wxWidgets بالفعل ، أو Boost.Asio كإطار عمل رئيسي للتطبيق ، فمن الممكن استخدام SObjectize في وحدة فرعية واحدة فقط من تطبيقك.


لدينا خبرة في استخدام SObjectizer لتطوير المكتبات التي تخفي استخدام SObjectizer كتفاصيل للتنفيذ. لم تكشف واجهة برمجة التطبيقات (API) العامة لتلك المكتبات عن وجود SObjectizer على الإطلاق. كان SObjectizer بالكامل تحت سيطرة مكتبة: المكتبة بدأت وأوقفت SObjectizer حسب الحاجة. تم استخدام هذه المكتبات في التطبيقات التي لم تكن على دراية تامة بوجود SObjectizer.


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


يمكنك تشغيل العديد من مثيلات SObjectizer في نفس الوقت


يسمح SObjectizer بتشغيل عدة مثيلات SObjectizer (تسمى SObjectizer Environment) في تطبيق واحد في نفس الوقت. ستكون كل بيئة SObjectizer مستقلة عن البيئات الأخرى المشابهة.


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


الموقتات جزء من SObjectizer


يعد دعم المؤقتات في شكل رسائل متأخرة ودورية من أحجار الزاوية في SObjectizer. يحتوي SObjectizer على العديد من تطبيقات آليات المؤقت (timer_wheel و timer_heap و timer_list) ويمكنه التعامل مع عشرات ومئات وآلاف الملايين من أجهزة ضبط الوقت في تطبيق ما. يمكن للمستخدم اختيار آلية توقيت الأنسب لتطبيق. علاوةً على ذلك ، يمكن للمستخدم توفير تطبيقه الخاص timer_thread / timer_manager إذا لم يكن أيٍّ من التطبيقات القياسية مناسبًا لظروف المستخدم.


SObjectizer لديه العديد من نقاط التخصيص وخيارات الضبط


يسمح SObjectizer بتخصيص العديد من الآليات المهمة. على سبيل المثال ، يمكن للمستخدم اختيار أحد التطبيقات القياسية لـ timer_thread (أو timer_manager). أو يمكن أن توفر التنفيذ الخاص بها. يمكن للمستخدم تحديد تطبيق كائنات القفل المستخدمة بواسطة قوائم انتظار الرسائل في مرسلات SObjectizer. أو يمكن أن توفر التنفيذ الخاص بها.


يمكن للمستخدم تنفيذ المرسل الخاص به. يمكن للمستخدم تنفيذ مربع الرسائل الخاص به. يمكن للمستخدم تنفيذ مظروف الرسائل الخاصة به. يمكن للمستخدم تنفيذ event_queue_hook الخاص به. و هكذا.


حيث SObjectizer يمكن أو لا يمكن استخدامها؟


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


حيث لا يمكن استخدام SObjectizer؟



كما قيل أعلاه ، فإن نماذج Actor و CSP ليست اختيارًا جيدًا للحوسبة عالية الأداء وغيرها من مجالات الحوسبة المتوازية. لذا ، إذا كنت بحاجة إلى مصفوفات متعددة أو تدفقات فيديو تحويل الشفرة ، فإن أدوات مثل OpenMP أو Intel TBB أو cpp-taskflow أو HPX أو MPI ستكون أكثر ملاءمة.


نظم الوقت الحقيقي الثابت


على الرغم من حقيقة أن SObjectizer له جذوره في أنظمة SCADA ، لا يمكن استخدام التطبيق الحالي لـ SObjectizer (المعروف أيضًا باسم SObjectizer-5) في أنظمة الوقت الحقيقي الصعبة. يرجع السبب الرئيسي في ذلك إلى استخدام الذاكرة الديناميكية في تطبيق SObjectizer: الرسائل هي كائنات مخصصة ديناميكيًا (ومع ذلك ، يمكن أن يستخدم SObjectizer الكائنات التي تم تخصيصها مسبقًا كرسائل) ، يستخدم المرسلون الذاكرة الديناميكية لقوائم انتظار الرسائل ، وحتى الحدود الزمنية لحالات الوكيل تستخدم الكائنات المخصصة ديناميكيًا لأداء فحص الوقت.


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


أنظمة مقيدة مضمن


يعتمد SObjectizer على مكتبة C ++ القياسية: std::thread يستخدم لإدارة الترابط ، std::atomic ، std::mutex ، std::condition_variable تستخدم لمزامنة البيانات ، RTTI و dynamic_cast يستخدم insize SObjectizer (على سبيل المثال ، std::type_index تستخدم لتحديد نوع الرسالة) ، يتم استخدام استثناءات C ++ للإبلاغ عن الأخطاء.


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


حيث تم استخدام SObjectizer في الماضي؟


نحاول الآن التحدث بإيجاز عن بعض حالات استخدام استخدام SObjectizer في الماضي (وليس فقط في الماضي). لسوء الحظ ، إنها ليست معلومات كاملة لأن هناك بعض المشاكل.


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


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


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


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

طعم SObjectizer


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


نموذج "Hello، World" التقليدي بأسلوب Actor Model


أبسط مثال مع وكيل واحد فقط يتفاعل مع رسالة hello وينهي عمله:


 #include <so_5/all.hpp> // Message to be sent to an agent. struct hello { std::string greeting_; }; // Demo agent. class demo final : public so_5::agent_t { void on_hello(mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; // Now agent can finish its work. so_deregister_agent_coop_normally(); } public: // There is no need is a separate constructor. using so_5::agent_t::agent_t; // Preparation of agent to work inside SObjectizer. void so_define_agent() override { // Subscription to 'hello' message. so_subscribe_self().event(&demo::on_hello); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Make and register an instance of demo agent. auto mbox = env.introduce_coop([](so_5::coop_t & coop) { auto * a = coop.make_agent<demo>(); return a->so_direct_mbox(); }); // Send hello message to registered agent. so_5::send<hello>(mbox, "Hello, World!"); }); } 

إصدار آخر من "Hello، World" مع وكلاء ونموذج نشر / اشتراك


أبسط مثال مع العديد من الوكلاء ، كلهم ​​يتفاعلون مع نفس مثيل رسالة hello :


 #include <so_5/all.hpp> using namespace std::string_literals; // Message to be sent to an agent. struct hello { std::string greeting_; }; // Demo agent. class demo final : public so_5::agent_t { const std::string name_; void on_hello(mhood_t<hello> cmd) { std::cout << name_ << ": greeting received: " << cmd->greeting_ << std::endl; // Now agent can finish its work. so_deregister_agent_coop_normally(); } public: demo(context_t ctx, std::string name, so_5::mbox_t board) : agent_t{std::move(ctx)} , name_{std::move(name)} { // Create a subscription for hello message from board. so_subscribe(board).event(&demo::on_hello); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Mbox to be used for speading hello message. auto board = env.create_mbox(); // Create several agents in separate coops. for(const auto & n : {"Alice"s, "Bob"s, "Mike"s}) env.register_agent_as_coop(env.make_agent<demo>(n, board)); // Spread hello message to all subscribers. so_5::send<hello>(board, "Hello, World!"); }); } 

إذا استخدمنا هذا المثال ، فيمكننا تلقي شيء من هذا القبيل:


 Alice: greeting received: Hello, World! Bob: greeting received: Hello, World! Mike: greeting received: Hello, World! 

مثال "Hello، World" بأسلوب CSP


دعونا نلقي نظرة على مثال SObjectizer دون أي ممثلين ، فقط std::thread والقنوات المشابهة لـ CSP.


نسخة بسيطة جدا


هذه نسخة بسيطة للغاية ليست آمنة:


 #include <so_5/all.hpp> // Message to be sent to a channel. struct hello { std::string greeting_; }; void demo_thread_func(so_5::mchain_t ch) { // Wait while hello received. so_5::receive(so_5::from(ch).handle_n(1), [](so_5::mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; }); } int main() { // Run SObjectizer in a separate thread. so_5::wrapped_env_t sobj; // Channel to be used. auto ch = so_5::create_mchain(sobj); std::thread demo_thread{demo_thread_func, ch}; // Send a greeting. so_5::send<hello>(ch, "Hello, World!"); // Wait for demo thread. demo_thread.join(); } 

أكثر قوة ، ولكن لا يزال إصدار بسيط


هذه نسخة معدلة من المثال الموضح أعلاه مع إضافة أمان استثناء:


 #include <so_5/all.hpp> // Message to be sent to a channel. struct hello { std::string greeting_; }; void demo_thread_func(so_5::mchain_t ch) { // Wait while hello received. so_5::receive(so_5::from(ch).handle_n(1), [](so_5::mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; }); } int main() { // Run SObjectizer in a separate thread. so_5::wrapped_env_t sobj; // Demo thread. We need object now, but thread will be started later. std::thread demo_thread; // Auto-joiner for the demo thread. auto demo_joiner = so_5::auto_join(demo_thread); // Channel to be used. This channel will be automatically closed // in the case of an exception. so_5::mchain_master_handle_t ch_handle{ so_5::create_mchain(sobj), so_5::mchain_props::close_mode_t::retain_content }; // Now we can run demo thread. demo_thread = std::thread{demo_thread_func, *ch_handle}; // Send a greeting. so_5::send<hello>(*ch_handle, "Hello, World!"); // There is no need to wait for something explicitly. } 

مثال HSM بسيط إلى حد ما: blinking_led


هذا مثال قياسي من توزيع SObjectizer. العامل الرئيسي في هذا المثال هو HSM يمكن وصفه بواسطة المخطط التالي:


الرسم البياني blinking_led


شفرة المصدر للمثال:


 #include <iostream> #include <so_5/all.hpp> class blinking_led final : public so_5::agent_t { state_t off{ this }, blinking{ this }, blink_on{ initial_substate_of{ blinking } }, blink_off{ substate_of{ blinking } }; public : struct turn_on_off final : public so_5::signal_t {}; blinking_led( context_t ctx ) : so_5::agent_t{ ctx } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; int main() { try { so_5::launch( []( so_5::environment_t & env ) { auto m = env.introduce_coop( []( so_5::coop_t & coop ) { auto led = coop.make_agent< blinking_led >(); return led->so_direct_mbox(); } ); auto pause = []( unsigned int v ) { std::this_thread::sleep_for( std::chrono::seconds{v} ); }; std::cout << "Turn blinking on for 10s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 10 ); std::cout << "Turn blinking off for 5s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 5 ); std::cout << "Turn blinking on for 5s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 5 ); std::cout << "Stopping..." << std::endl; env.stop(); } ); } catch( const std::exception & ex ) { std::cerr << "Error: " << ex.what() << std::endl; } return 0; } 

أجهزة ضبط الوقت والتحكم في التحميل الزائد لوكيل ومرسل active_obj


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


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


 #include <so_5/all.hpp> using namespace std::chrono_literals; // Message to be sent to the consumer. struct task { int task_id_; }; // An agent for utilization of unhandled tasks. class trash_can final : public so_5::agent_t { public: // There is no need is a separate constructor. using so_5::agent_t::agent_t; // Preparation of agent to work inside SObjectizer. void so_define_agent() override { // Subscription to 'task' message. // Event-handler is specified in the form of a lambda-function. so_subscribe_self().event([](mhood_t<task> cmd) { std::cout << "unhandled task: " << cmd->task_id_ << std::endl; }); } }; // The consumer of 'task' messages. class consumer final : public so_5::agent_t { public: // We need the constructor. consumer(context_t ctx, so_5::mbox_t trash_mbox) : so_5::agent_t{ctx + // Only three 'task' messages can wait in the queue. limit_then_redirect<task>(3, // All other messages will go to that mbox. [trash_mbox]{ return trash_mbox; })} { // Define a reaction to incoming 'task' message. so_subscribe_self().event([](mhood_t<task> cmd) { std::cout << "handling task: " << cmd->task_id_ << std::endl; std::this_thread::sleep_for(75ms); }); } }; // The producer of 'test' messages. class producer final : public so_5::agent_t { const so_5::mbox_t dest_; so_5::timer_id_t task_timer_; int id_counter_{}; // Type of periodic signal to produce new 'test' message. struct generate_next final : public so_5::signal_t {}; void on_next(mhood_t<generate_next>) { // Produce a new 'task' message. so_5::send<task>(dest_, id_counter_); ++id_counter_; // Should the work be stopped? if(id_counter_ >= 10) so_deregister_agent_coop_normally(); } public: producer(context_t ctx, so_5::mbox_t dest) : so_5::agent_t{std::move(ctx)} , dest_{std::move(dest)} {} void so_define_agent() override { so_subscribe_self().event(&producer::on_next); } // This method will be automatically called by SObjectizer // when agent starts its work inside SObjectizer Environment. void so_evt_start() override { // Initiate a periodic message with no initial delay // and repetition every 25ms. task_timer_ = so_5::send_periodic<generate_next>(*this, 0ms, 25ms); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Make and register coop with agents. // All agents will be bound to active_obj dispatcher and will // work on separate threads. env.introduce_coop( so_5::disp::active_obj::make_dispatcher(env).binder(), [](so_5::coop_t & coop) { auto * trash = coop.make_agent<trash_can>(); auto * handler = coop.make_agent<consumer>(trash->so_direct_mbox()); coop.make_agent<producer>(handler->so_direct_mbox()); }); }); } 

إذا قمنا بتشغيل هذا المثال ، فيمكننا رؤية الإخراج التالي:


 handling task: 0 handling task: 1 unhandled task: 5 unhandled task: 6 handling task: 2 unhandled task: 8 unhandled task: 9 handling task: 3 handling task: 4 handling task: 7 

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


المزيد من الأمثلة


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


ماذا عن الأداء؟


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


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


إذا كنت تريد أرقامًا من بعض المقاييس الاصطناعية ، فهناك بعض البرامج في مجلد test / so_5 / bench لتوزيع SObjectizer.


ملاحظة حول المقارنة مع أدوات مختلفة


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


لذلك نترك مقارنة السرعة لأولئك الذين يحبون تلك اللعبة ولديهم وقت للعبها.


لماذا يبدو SObjectizer تمامًا كما هو؟


هناك العديد من "أطر الفاعل" لـ C ++ ، وكلها تبدو مختلفة. يبدو أن لديها بعض الأسباب الموضوعية: كل إطار له ميزات فريدة وأهداف مختلفة. علاوة على ذلك ، يمكن تنفيذ العناصر الفاعلة في C ++ بشكل مختلف جدًا. لذا فإن السؤال الرئيسي هو "لماذا لا يبدو الإطار X كإطار Y؟" ، ولكن "لماذا يبدو الإطار X كما هو؟"


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


الوكلاء هم كائنات الفئات المشتقة من agent_t


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


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


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


الرسائل ككائنات لهياكل / فئات المستخدم


الرسائل في SObjectizer هي كائنات بنيات أو فئات معرفة من قبل المستخدم. هناك سببان على الأقل لذلك:


  • بدأ تطوير SObjectizer-5 في عام 2010 عندما لم تكن C ++ 11 موحدة حتى الآن. لذلك في البداية ، لم نتمكن من استخدام ميزات C ++ 11 كقوالب متغيرة وفئة std::tuple . كان الخيار الوحيد الذي كان لدينا هو استخدام كائن من فئة موروثة من message_t فئة خاصة. الآن ليست هناك حاجة لاشتقاق نوع الرسالة من message_t ، لكن SObjectizer يلتف كائن المستخدم في كائن message -_tived على أي حال تحت غطاء محرك السيارة ؛
  • يمكن بسهولة تغيير محتوى الرسالة دون تعديل تواقيع معالجات الأحداث. وهناك عنصر تحكم من برنامج التحويل البرمجي: إذا قمت بإزالة بعض الحقول من رسالة أو غيرت نوعها ، فسيعلمك المجمع عن وصول خاطئ إلى هذا الحقل.

يسمح استخدام الرسائل ككائنات أيضًا بالعمل مع الرسائل التي تم تخصيصها مسبقًا وتخزين رسالة مستلمة على بعض الحاوية وإعادة إرسالها لاحقًا.


مجموعات من العملاء


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


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


يمكن أيضًا رؤية حظيرة كمشرف في وضع الكل في واحد: إذا فشل وكيل من حظيرة ثم تتم إزالة حظيرة كاملة من بيئة SObjectizer وتدميرها (يمكن للمستخدم التفاعل مع ذلك وإعادة إنشاء حظيرة مرة أخرى).


صناديق الرسائل


مربعات الرسائل هي ميزة SObjectizer فريدة أخرى. يتم إرسال الرسائل في SObjectizer إلى مربع رسالة (mbox) ، وليس إلى وكيل مباشرة. يمكن أن يكون هناك جهاز استقبال واحد خلف mbox ، أو يمكن أن يكون هناك مليون مشترك ، أو لا يمكن أن يكون هناك أحد.


تتيح لنا Mboxes دعم الوظيفة الأساسية لنموذج Publish-Subsc. يمكن اعتبار mbox وسيط MQ ويمكن رؤية نوع الرسالة كموضوع.


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


وكلاء كما HSM


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


تمت إضافة دعم آلات الحالة الهرمية (مثل معالجات on_enter / on_exit ، والحالات المتداخلة ، والحدود الزمنية وما إلى ذلك) بعد مرور بعض الوقت على استخدام SObjectizer في الإنتاج. وهذه الميزة جعلت SObjectizer أداة أكثر قوة ومريحة.


استخدام استثناءات C ++


يتم استخدام استثناءات C ++ في SObjectizer كآلية الإبلاغ عن الخطأ الرئيسية. على الرغم من أن استخدام استثناء C ++ يمكن أن يكون مكلفًا في بعض الأحيان ، فقد قررنا استخدام استثناءات بدلاً من رموز الأخطاء.


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


يتيح استخدام استثناءات C ++ في SObjectizer-5 كتابة رمز أكثر صحة وقوة. في الحالات المعتادة ، نادرًا ما يتم طرح استثناءات بواسطة SObjectizer ، لذا فإن استخدام الاستثناء ليس له أي تأثير سلبي على أداء SObjectizer أو أداء التطبيقات المكتوبة أعلى SObjectizer.


لا يوجد دعم للتطبيقات الموزعة "خارج الصندوق"


لا يحتوي SObjectzer-5 على دعم مضمن للتطبيقات الموزعة. وهذا يعني أن SObjectizer يوزع الرسائل فقط داخل عملية واحدة. إذا كنت بحاجة إلى تنظيم توزيع رسائل inter-process أو inter-note ، فعليك دمج نوع من IPC في التطبيق الخاص بك.


هذا ليس لأننا لا نستطيع تطبيق بعض أشكال IPC في SObjectizer. كان لدينا بالفعل ذلك في SObjectizer-4. ولأن لدينا مثل هذه التجربة ، فقد قررنا عدم القيام بذلك في SObjectizer-5. لقد تعلمنا أنه لا يوجد نوع واحد من IPC يناسب تمامًا الظروف المختلفة.


إذا كنت ترغب في الحصول على اتصال جيد بين العقدة في تطبيقك ، فعليك تحديد البروتوكولات الأساسية المناسبة. على سبيل المثال ، إذا كان عليك نشر ملايين الحزم الصغيرة مع بعض البيانات قصيرة الأجل (مثل توزيع قياس الظروف الجوية الحالية) فعليك استخدام IPC واحد. ولكن إذا كان عليك نقل BLOBs الضخمة (مثل دفق الفيديو 4K / 8K أو المحفوظات مع البيانات المالية داخل) ثم عليك استخدام نوع IPC آخر.


ونحن لا نتحدث عن روح المبادرة مع البرامج المكتوبة بلغات مختلفة ...


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


يسمح SObjectizer بدمج أنواع مختلفة من IPC في شكل mboxes مخصص. لذلك يسمح بإخفاء حقيقة توزيع الرسائل عبر شبكة من مستخدمي SObjectizer.


بدلا من الاستنتاج


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


SObjectizer نفسها تعيش على جيثب . هناك Wiki للمشروع على GitHub ونوصي بالبدء من SObjectizer 5.6 Basics ومن ثم الانتقال إلى مقالات من سلسلة In- deep . بالنسبة لأولئك الذين يرغبون في التعمق أكثر ، يمكننا أن نوصي دعونا ننظر تحت قسم غطاء محرك السيارة SObjectizer .


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

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


All Articles