التخطيط في الذهاب: الجزء الثاني - The Go Scheduler

مرحبا يا هابر! هذه هي المشاركة الثانية في سلسلة من ثلاثة أجزاء ، والتي ستعطي فكرة عن ميكانيكا ودلالات عمل المجدول في Go. هذا المنشور عن مخطط Go.

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

يبدأ برنامجك


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

صورة

يمكنك أن ترى أن لدي معالج واحد مع 4 نوى مادية. لا يكشف هذا التقرير عن عدد مؤشرات ترابط الأجهزة لكل نواة مادية. يحتوي معالج Intel Core i7 على تقنية Hyper-Threading ، مما يعني أن النواة المادية بها 2 خيط من الأجهزة. هذا يخبر Go أن 8 نوى افتراضية متاحة لتشغيل خيوط نظام التشغيل بالتوازي. للتحقق من ذلك ، خذ بعين الاعتبار البرنامج التالي:

package main

import (
	"fmt"
	"runtime"
)

func main() {

    // NumCPU returns the number of logical
    // CPUs usable by the current process.
    fmt.Println(runtime.NumCPU())
}

عند تشغيل هذا البرنامج على جهاز الكمبيوتر الخاص بي ، ستكون نتيجة استدعاء وظيفة NumCPU () 8. أي برنامج Go يتم تشغيله على جهاز الكمبيوتر الخاص بي سيحصل على 8 (P). يتم تعيين دفق OS ( M )
لكل P. لا يزال نظام التشغيل هذا يديره نظام التشغيل ، ولا يزال نظام التشغيل مسؤولاً عن وضع مؤشر الترابط في النواة للتنفيذ. هذا يعني أنه عندما أقوم بتشغيل Go على جهاز الكمبيوتر الخاص بي ، فإن لدي 8 سلاسل متاحة لأداء عملي ، كل منها مرتبط بشكل فردي بـ P. يتم إعطاء كل برنامج Go Goorine الأولي ( G

) Goroutine هو في الأساس Coroutine ، ولكنه Go ، لذلك نستبدل الحرف C بالحرف G ونحصل على كلمة Goroutine. يمكنك التفكير في Goroutines كخيوط على مستوى التطبيق ، وهي تشبه إلى حد كبير سلاسل نظام التشغيل. مثلما تقوم النواة بتشغيل أو إيقاف تشغيل سلاسل نظام التشغيل ، يتم تشغيل برامج السياق وإيقاف تشغيلها بواسطة السياق.

اللغز الأخير هو طوابير التنفيذ. هناك نوعان من قوائم انتظار التنفيذ المختلفة في جدولة Go: قائمة انتظار التنفيذ العامة (GRQ) وقائمة انتظار التنفيذ المحلية (LRQ). يتم تعيين LRQ لكل P يتحكم في goroutines المعينة للتنفيذ في سياق P. يتم تشغيل وإيقاف تشغيل goroutines من السياق M المعين لهذا P. GRQ للغوروتينات التي لم يتم تعيينها لـ P. هناك عملية لنقل goroutines من GRQ إلى LRQ ، والذي سنناقشه لاحقًا.

تُظهر الصورة كل هذه المكونات معًا.

صورة

مخطط تعاوني


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

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

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

دول Gorutin


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

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

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

وفاء : يعني ذلك أن goroutine تم وضعها في M ويتابع تعليماتها. تم الانتهاء من العمل المرتبط بالطلب. هذا ما يريده الجميع.

تبديل السياق


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

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

  • باستخدام الكلمة الرئيسية go
  • جامع القمامة
  • مكالمات النظام
  • التزامن

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

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

مكالمات النظام
إذا goroutine يجعل استدعاء نظام من شأنها أن تجعل من منع M، يمكن جدولة تبديل السياق إلى goroutine آخر، إلى نفس M.

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

مكالمات النظام غير المتزامن


عندما يكون لدى نظام التشغيل الذي تعمل عليه القدرة على معالجة مكالمة النظام بشكل غير متزامن ، يمكن استخدام ما يسمى وحدة تحكم الشبكة لمعالجة استدعاء النظام بشكل أكثر كفاءة. يتم تحقيق ذلك باستخدام kqueue (MacOS) أو epoll (Linux) أو iocp (Windows) في أنظمة التشغيل هذه.

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

أفضل طريقة لمعرفة كيفية عمل ذلك هي إلقاء نظرة على مثال. يوضح الشكل مخطط التخطيط الأساسي لدينا. يتم تنفيذ Gorutin-1 على M ، و 3 آخرين من Gorutins ينتظرون في LRQ للحصول على وقتهم على M. Network poller خاملاً ، وليس لديه ما يفعله.

صورة

في الشكل التالي ، يريد Gorutin-1 (G1) إجراء مكالمة نظام شبكة ، بحيث تنتقل G1 إلى وحدة تحكم الشبكة ويتم التعامل معها على أنها استدعاء نظام شبكة غير متزامن. بمجرد أن يتم نقل G1 إلى Network poller ، يتوفر M الآن لتنفيذ goroutine آخر من LRQ. في هذه الحالة ، يتحول Gorutin-2 إلى M.

صورة

في الشكل التالي ، تنتهي مكالمة شبكة النظام بمكالمة شبكة غير متزامنة ، وينتقل G1 إلى LRQ لـ P. بعد إعادة G1 إلى M ، الرمز المرتبط بـ Go ، والذي يجيب يمكن تنفيذ مرة أخرى. والفوز الكبير هو أنه لا يلزم السيدة إضافية لإجراء مكالمات نظام الشبكة. يحتوي poller الشبكة على مؤشر ترابط نظام التشغيل ، ويعالج من خلال حلقة حدث.

مكالمات النظام المتزامن


ماذا يحدث عندما يريد goroutine إجراء مكالمة نظام لا يمكن تنفيذها بشكل غير متزامن؟ في هذه الحالة ، لا يمكن استخدام poller Network ، وسيحظر goroutine الذي يقوم باستدعاء النظام M. هذا سيئ ، ولكن لا توجد طريقة لمنع ذلك. أحد الأمثلة على استدعاء النظام الذي لا يمكن إجراؤه بشكل غير متزامن هو مكالمات النظام المستندة إلى الملفات. إذا كنت تستخدم CGO ، فقد تكون هناك مواقف أخرى تمنع فيها وظائف C أيضًا.
يمكن لنظام التشغيل Windows إجراء مكالمات نظام غير متزامنة قائمة على الملفات. تقنيًا ، عند العمل على Windows ، يمكنك استخدام Network poller.
دعنا نرى ما يحدث مع استدعاء نظام متزامن (على سبيل المثال ، ملف I / O) سيحظر M. يوضح الشكل الرسم التخطيطي الأساسي للتخطيط ، ولكن هذه المرة ستقوم G1 بإجراء مكالمة نظام متزامنة ستحظر M1.

صورة

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

صورة

تكمل الخطوة التالية استدعاء نظام القفل الذي أجرته G1. عند هذه النقطة ، يمكن أن تعود G1 إلى LRQ ويتم تقديمها مرة أخرى بواسطة P. M1 ثم تنحى جانباً للاستخدام المستقبلي إذا تكرر هذا السيناريو.

صورة

سرقة العمل


جانب آخر من المجدول هو أنه مخطط سرقة غوروتين. هذا يساعد في العديد من المجالات لدعم التخطيط الفعال. أولاً ، آخر شيء تحتاجه هو أن ينتقل M إلى حالة الاستعداد ، لأنه بمجرد حدوث ذلك ، سيقوم نظام التشغيل بتبديل M من النواة باستخدام السياق. هذا يعني أن P لا يمكنه القيام بأي عمل ، حتى إذا كان هناك Goroutine في حالة صحية ، حتى يتحول M إلى النواة. يساعد Gorutin Theft أيضًا على موازنة الفترات الزمنية بين جميع Ps بحيث يتم توزيع العمل بشكل أفضل وأداء أكثر كفاءة.

في الشكل ، لدينا برنامج Go متعدد الخيوط مع اثنين من Ps يخدم أربعة Gs لكل منهما وواحد G في GRQ. ماذا يحدث إذا كان أحد P يخدم G بسرعة؟

صورة

علاوة على ذلك ، لم يعد P1 لديه goroutines للتنفيذ. ولكن هناك goroutines في حالة العمل ، سواء في LRQ ل P2 ، وفي GRQ. هذه هي اللحظة التي يحتاج فيها P1 لسرقة الغوروتين.

صورة

قواعد سرقة الغوروتين هي كما يلي. يمكن عرض جميع التعليمات البرمجية في مصادر وقت التشغيل.

runtime.schedule() {
    // only 1/61 of the time, check the global runnable queue for a G.
    // if not found, check the local queue.
    // if not found,
    //     try to steal from other Ps.
    //     if not, check the global runnable queue.
    //     if not found, poll network.
}

وبالتالي ، بناءً على هذه القواعد ، يجب على P1 التحقق من P2 بحثًا عن وجود goroutines في LRQ الخاص به وأخذ نصف ما يعثر عليه.

صورة

ماذا يحدث إذا أنهى P2 خدمة جميع برامجه ولم يتبق P1 في LRQ؟

أكمل P2 جميع أعماله والآن يجب أن يسرق الغوروتينات. أولاً ، سوف ينظر إلى LRQ P1 ، لكنه لن يجد أي Goroutines. بعد ذلك سوف يلقي نظرة على GRQ. هناك سيجد G9.

صورة

P2 يسرق G9 من GRQ ويبدأ في القيام بالمهمة. ما هو جيد في كل هذه السرقة هو أنه يسمح لـ M بالبقاء مشغولًا وعدم النشاط.

صورة

مثال عملي


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

صورة

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

صورة

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

صورة

يوجد في الشكل نوعان من الغوروتينات متناغمان مع بعضهما البعض ، ويمرران الرسالة ذهابًا وإيابًا. يحصل G1 على مفتاح السياق M1 ، الذي يعمل على Core 1 ، والذي يسمح لـ G1 بالقيام بعمله.

صورة

علاوة على ذلك ، عندما ينتهي G1 من إرسال الرسالة ، فإنه يحتاج الآن إلى انتظار الرد. سيؤدي ذلك إلى فصل G1 عن سياق M1 ووضعه في حالة الخمول. بمجرد إخطار G2 بالرسالة ، فإنها تصبح في حالة صحية. الآن يمكن لجدولة Go إجراء تبديل السياق وتشغيل G2 على M1 ، والذي لا يزال يعمل على Core 1. ثم تعالج G2 الرسالة وترسل رسالة جديدة إلى G1.

صورة

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

صورة

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

إذا تم استخدام goroutine ، يتم استخدام نفس سلاسل نظام التشغيل والنواة لجميع المعالجة. هذا يعني أنه من وجهة نظر نظام التشغيل ، لا يدخل OS Flow أبدًا في حالة الانتظار ؛ أبدا. ونتيجة لذلك ، لا تضيع جميع التعليمات التي فقدناها عند تبديل السياقات عند استخدام التدفقات عند استخدام goroutin.

بشكل أساسي ، حوّل Go عمل IO / Blocking إلى مهمة مرتبطة بالمعالج على مستوى نظام التشغيل. نظرًا لأن كل تبديل السياق يحدث على مستوى التطبيق ، فإننا لا نفقد نفس التعليمات ~ 12 ألف تعليمات (في المتوسط) حول تبديل السياق التي فقدناها عند استخدام التدفقات. في Go ، تكلفك مفاتيح تبديل السياق نفسها ~ 200 نانو ثانية أو ~ 2.4 ألف أمر. يساعد المجدول أيضًا على تحسين أداء سلاسل التخزين المؤقت و NUMA. لهذا السبب لا نحتاج إلى سلاسل رسائل أكثر مما لدينا نوى افتراضية. يمكن لـ Go القيام بمزيد من العمل بمرور الوقت ، لأن برنامج جدولة Go يحاول استخدام عدد أقل من سلاسل الرسائل والقيام بالمزيد في كل سلسلة رسائل ، مما يساعد على تقليل الحمل على نظام التشغيل والأجهزة.

استنتاج


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

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

All Articles