المزيد عن Coroutines في C ++

مرحبا زملائي.

كجزء من تطوير سمة C ++ 20 ، صادفنا في وقت ما مقالًا قديمًا إلى حد ما (سبتمبر 2018) من مدونة Yandex ، والتي تسمى " Getting Ready for C ++ 20. Coroutines TS with Real Example ". وينتهي الأمر بالتصويت المعبّر التالي:



"لماذا لا" ، قرّرنا وترجمنا مقالة بقلم ديفيد بيلارسكي تحت عنوان "مقدمة Coroutines". تم نشر المقال قبل أكثر من عام بقليل ، ولكن نأمل أن تجده مثيرًا للاهتمام على أي حال.

لذلك حدث ذلك. بعد الكثير من الشك والجدل والتحضير لهذه الميزة ، توصلت WG21 إلى رأي عام حول الشكل الذي يجب أن تبدو عليه Coruteines في C ++ - ومن المحتمل جدًا أن يتم تضمينها في C ++ 20. نظرًا لأن هذه ميزة رئيسية ، أعتقد أن الوقت قد حان لإعدادها ودراستها بالفعل الآن (كما تتذكر ، لا يزال هناك المزيد من الوحدات والمفاهيم والنطاقات للتعلم ...)

لا يزال العديد يعارضون coroutine. غالبًا ما يشتكون من تعقيد تطورهم ، والعديد من نقاط التخصيص ، وربما الأداء دون المستوى الأمثل ، ربما بسبب التخصيص غير المحسن للذاكرة الديناميكية (ربما ؛)).

بالتوازي مع تطوير المواصفات الفنية المعتمدة (المنشورة رسميًا) (TS) ، تم إجراء محاولات للتطوير الموازي لآلية أخرى للكوروتين. هنا سوف نتحدث عن تلك Coruteines الموصوفة في TS ( المواصفات الفنية ). نهج بديل ، بدوره ، ينتمي إلى Google. ونتيجة لذلك ، اتضح أن نهج Google يعاني من العديد من المشكلات ، والتي يتطلب حلها في الغالب ميزات إضافية غريبة لـ C ++.

في النهاية ، تقرر اعتماد نسخة من الكوروتين الذي طوره Microsoft (مؤلفو TS). حول مثل هذه coruteines التي سيتم مناقشتها في هذه المقالة. لذا ، لنبدأ بسؤال ...

ما هي coroutines؟


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

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

  • Coroutines مكدسة
  • المكدس coroutines
  • تيارات خضراء
  • الألياف
  • جوروتينز

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

لفهم coroutines ، بما في ذلك على مستوى بديهية ، دعونا نتعرف على الوظائف لفترة وجيزة (دعونا نضعها بهذه الطريقة) "API الخاصة بهم". الطريقة القياسية للعمل معهم هي الاتصال والانتظار حتى الانتهاء:

void foo(){
     return; //     
}	
foo(); //   / 

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

مع coroutines ، الوضع مختلف. لا يمكنك فقط بدءها وإيقافها ، ولكن أيضًا إيقافها مؤقتًا واستئنافها. لا تزال مختلفة عن التدفقات الأساسية ، لأن الكوروتونات نفسها لا تزدحم (من ناحية أخرى ، تشير الكوروتونات عادة إلى التدفق ، والتدفق يزاحم). لفهم ذلك ، ضع في اعتبارك مولدًا محددًا في Python. دع هذا الشيء يسمى مولد في Python ، في C ++ سيطلق عليه coroutine. يتم أخذ مثال من هذا الموقع :

def generate_nums():
     num = 0
     while True:
          yield num
          num = num + 1	

nums = generate_nums()
	
for x in nums:
     print(x)
	
     if x > 9:
          break

إليك كيفية عمل هذا الرمز: generate_numsيؤدي استدعاء دالة إلى إنشاء كائن coroutine. في كل خطوة من خطوات تعداد كائن coroutine ، يستأنف coroutine نفسه العمل ويوقفه مؤقتًا فقط بعد كلمة رئيسية yieldفي الشفرة ؛ ثم يتم إرجاع العدد الصحيح التالي من التسلسل (بالنسبة للحلقة هي السكر النحوي لاستدعاء دالة next()تستأنف coroutine). ينهي الكود الحلقة بمصادفة عبارة استراحة. في هذه الحالة ، لا ينتهي كوروتين أبدًا ، ولكن من السهل تخيل موقف يصل فيه كوروتين إلى النهاية وينتهي. وكما نرى، إلى العمليات المطبقة korutine start، suspend، resumeوأخيرا،finish. [ملاحظة: توفر C ++ أيضًا عمليات الإنشاء والتدمير ، ولكنها ليست مهمة في سياق الفهم الحدسي للكوروتين).

Coroutines كمكتبة


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

هنا نحاول الإجابة على هذا السؤال وتوضيح الفرق بين Coruteines المكدسة والمكدسة. هذا الاختلاف هو مفتاح فهم corutin كجزء من اللغة.

المكدس coroutines


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

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

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

تنتج التأثيرات التالية عن الخصائص المذكورة أعلاه:

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

الآن دعونا نلقي نظرة فاحصة على تشغيل الألياف ونشرح أولاً كيف يشارك المكدس في استدعاءات الوظائف.

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

بعض هذه السجلات لها مهام خاصة ، وعند استدعاء وظائف ، يتم تخزينها على المكدس. هذه هي السجلات (في حالة بنية ARM):

SP - مؤشر مكدس
LR -
كمبيوتر تسجيل الاتصالات - مؤشر مكدس عداد البرامج

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

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

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



يسجل سلوك الكمبيوتر الشخصي و LR عند استدعاء دالة

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



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

  1. استدعاء دالة عادية داخل خيط. يتم تخصيص الذاكرة على المكدس.
  2. . . , . . , .
  3. .
  4. . .
  5. .
  6. .
  7. . , , , .
  8. .
  9. .
  10. – , .
  11. , .
  12. . .
  13. . : , . , ( ) .
  14. , .
  15. .
  16. . . . , .
  17. .
  18. , , .

عند العمل مع المكدس ، لا حاجة لميزة لغة مخصصة تضمن استخدامها. يتم تنفيذ حزمة korutiny المكدسة بالكامل باستخدام المكتبات ، والمكتبات الموجودة بالفعل مصممة خصيصًا لهذا الغرض:

swtch.com/libtask
code.google.com/archive/p/libconcurrency
www.boost.org Boost.Fiber
www.boost.org the Boost .Coroutine

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

فكر في مثال Boost.Fiber:

#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
	
#include <boost/intrusive_ptr.hpp>
	
#include <boost/fiber/all.hpp>
	
inline
void fn( std::string const& str, int n) {
     for ( int i = 0; i < n; ++i) {
          std::cout << i << ": " << str << std::endl;
               boost::this_fiber::yield();
     }
}
	
int main() {
     try {
          boost::fibers::fiber f1( fn, "abc", 5);
          std::cerr << "f1 : " << f1.get_id() << std::endl;
          f1.join();
          std::cout << "done." << std::endl;
	
          return EXIT_SUCCESS;
     } catch ( std::exception const& e) {
          std::cerr << "exception: " << e.what() << std::endl;
     } catch (...) {
          std::cerr << "unhandled exception" << std::endl;
     }
     return EXIT_FAILURE;
}

في حالة Boost.Fiber ، تحتوي المكتبة على جدولة مضمنة لـ coroutine. تعمل جميع الألياف في نفس الخيط. نظرًا لأن تخطيط Corutin يكون تعاونيًا ، يجب أن تحدد الألياف أولاً متى ستعيد التحكم إلى المجدول. في هذا المثال ، يحدث هذا عندما يتم استدعاء دالة العائد ، مما يؤدي إلى إيقاف coroutine مؤقتًا.

نظرًا لعدم وجود ألياف أخرى ، يقرر مخطط الألياف دائمًا استئناف Coroutine.

Coroutines مكدسة


تختلف Coruteines المكدسة قليلاً في الخصائص عن المكدس. ومع ذلك ، فإن خصائصها الأساسية هي نفسها ، حيث يمكن أيضًا بدء Coruteines غير المكدسة ، وبعد استئناف تعليقها. من المحتمل أن نجد Coroutines من هذا النوع في C ++ 20.

إذا تحدثنا عن الخصائص المماثلة للكوروتين - يمكن للكوروتينات أن:

  • ترتبط Corutin ارتباطًا وثيقًا بمتصلها: عندما يتم استدعاء coroutine ، يتم نقل التنفيذ إليها ، ويتم نقل نتيجة coroutine إلى المتصل.
  • مدى عمر مكدس corutin مساوٍ لحياة مكدسه. عمر كوروتين غير مكدس مساوٍ لحياة جسمه.

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

بادئ ذي بدء ، إذا لم يخصصوا ذاكرة للمكدس ، فكيف تعمل؟ حيث في جميع الحالات ، يتم نقل جميع البيانات التي يجب تخزينها على المكدس عند العمل مع المكدس coroutines. الجواب: على كومة المتصل.

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

ألق نظرة على كيفية عمل corutins بدون تكديس:



تحدي corutin بدون تكديس

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

  1. استدعاء دالة عادي يتم تخزين إطاره على المكدس
  2. تخلق الوظيفة كوروتين . أي أنه يخصص إطار تنشيط له في مكان ما على كومة الذاكرة المؤقتة.
  3. استدعاء دالة عادية.
  4. اتصل بـ Corutin . يبرز جسد Corutin في كومة منتظمة. يتم تنفيذ البرنامج بنفس الطريقة كما في حالة الوظيفة المنتظمة.
  5. استدعاء دالة عادية من coroutine. مرة أخرى ، لا يزال كل شيء يحدث على المكدس [ملاحظة: لا يمكنك إيقاف Coroutine مؤقتًا من هذه النقطة ، لأن هذه ليست الوظيفة الأعلى في coroutine]
  6. [: .]
  7. – , , .
  8. – , + .
  9. 5.
  10. 6.
  11. . .

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

يمكن أن يتسبب Corutin أيضًا في coroutines الأخرى (غير موضحة في هذا المثال). في حالة coroutines مكدسة ، كل مكالمة تؤدي إلى تخصيص مساحة جديدة لبيانات corutin الجديدة (مع مكالمة متكررة من coroutine ، يمكن تخصيص الذاكرة الديناميكية عدة مرات).

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

الاستخدام العملي للكوروتين


يمكن استخدام Coroutines في C ++ بنفس الطرق المستخدمة في اللغات الأخرى. ستعمل Coroutines على تبسيط الإملاء:

  • مولدات كهرباء
  • كود الإدخال / الإخراج غير المتزامن
  • الحوسبة البطيئة
  • تطبيقات مدفوعة بالحدث

ملخص


آمل أنه من خلال قراءة هذا المقال ستجد:

  • لماذا في C ++ تحتاج إلى تنفيذ Coroutines كميزة لغوية مخصصة
  • ما الفرق بين coruteines مكدسة و stacked؟
  • لماذا هناك حاجة coroutines

All Articles