كانت الشعبية الهائلة لمصطلح "غير متزامن" في موجة الويب 2.0 ، والتي اجتاحت الويب مع JavaScript و Ajax. لكن عدم التزامن نادر في معظم لغات البرمجة رفيعة المستوى. يعكس PHP أفضل هذه الميزة: فهي لا تمنع فقط بشكل غير متزامن ، ولكن أيضًا لا توفر مؤشرات ترابط متعددة. يتم تنفيذ PHP بطريقة حظر متزامنة. هذه المزايا مفيدة للمبرمجين لكتابة منطق العمل بالتسلسل ، ولكن في تطبيقات الشبكة المعقدة ، يؤدي حظرها إلى أن تكون أكثر تزامن.
على جانب الخادم ، يكون I/O مكلفًا للغاية ، وتوزيع الإدخال/الإخراج أغلى ثمناً. فقط عندما يمكن للواجهة الخلفية الاستجابة للموارد بسرعة ، يمكن أن تصبح التجربة الأمامية أفضل. Node.js هي أول منصة تستخدم غير متزامن كطريقة البرمجة الرئيسية ومفهوم التصميم. يرافقه الإدخال/الإخراج غير المتزامن ، الذي يحركه الأحداث والرواية الفردية ، يشكلون نغمة العقدة. ستقدم هذه المقالة كيف تنفذ العقدة I/O غير متزامن.
1. المفاهيم الأساسية
يبدو "Async" و "عدم الحظر" نفس الشيء ، ومن حيث النتائج الفعلية ، يحقق كلاهما الغرض من التوازي. ولكن من منظور kernel i/o ، هناك طريقتان فقط: الحظر وغير المحظور. لذلك غير متزامن/متزامن وحظر/عدم الحظر هما في الواقع شيئان مختلفان.
1.1 حظر الإدخال/الإخراج وغير المحظور I/O
تتمثل إحدى ميزات حظر الإدخال/الإخراج في أنه بعد الاتصال ، يجب أن تنتظر حتى يتم الانتهاء من جميع العمليات على مستوى kernel النظام قبل الانتهاء من المكالمة. أخذ قراءة ملف على القرص كمثال ، تنتهي هذه المكالمة بعد أن تكمل kernel النظام البحث عن القرص ، ويقرأ البيانات ، ونسخ البيانات في الذاكرة.
يؤدي حظر الإدخال/الإخراج إلى انتظار وحدة المعالجة المركزية لإدخال/إخراج ، وإهدار وقت الانتظار ، ولا يمكن استخدام قوة معالجة وحدة المعالجة المركزية بالكامل. تتمثل خاصية I/O غير المحظورة في أنه سيعود مباشرة بعد المكالمة ، ويمكن استخدام شريحة وقت وحدة المعالجة المركزية للتعامل مع المعاملات الأخرى بعد العودة. نظرًا لعدم اكتمال I/O الكامل ، فإن ما يتم إرجاعه على الفور ليس البيانات التي تتوقعها طبقة العمل ، ولكن فقط حالة المكالمة الحالية. من أجل الحصول على البيانات الكاملة ، يحتاج التطبيق إلى استدعاء عملية الإدخال/الإخراج بشكل متكرر لتأكيد ما إذا كان قد اكتمل (أي الاقتراع). تحتاج تقنيات الاقتراع إلى ما يلي:
1. القراءة: التحقق من حالة الإدخال/الإخراج عن طريق المكالمات المتكررة هو أكثر طريقة الأداء الأصلية والأقل
2. حدد: تحسينات للقراءة ، احكم على حالة الحدث على واصف الملف. العيب هو أن الحد الأقصى لعدد واصفات الملفات محدود.
3. POLL: تحسينات في تحديد ، باستخدام قوائم مرتبطة لتجنب الحد الأقصى لأرقام الرقم ، ولكن عندما يكون هناك العديد من الواصفات ، لا يزال الأداء منخفضًا جدًا
4.Poll: إذا لم يتم فحص حدث I/O أثناء الاقتراع ، فسوف ينام حتى يحدث الحدث ويوقظه. هذه هي آلية إشعار الأحداث الأكثر كفاءة بموجب Linux.
يلبي الاقتراع الحاجة إلى عدم الحظر الإدخال/الإخراج لضمان اكتساب البيانات الكامل ، ولكن للتطبيقات ، لا يزال بإمكانه الاعتماد كنوع من المزامنة لأنه لا يزال يحتاج إلى انتظار إدخال الإدخال/الإخراج تمامًا. أثناء الانتظار ، تُستخدم وحدة المعالجة المركزية إما لاجتياز حالة واصف الملف أو في انتظار حدوث الأحداث.
1.2 I/O غير متزامن في المثالية والواقع
يجب أن يكون الإدخال/الإخراج غير المتزامن المثالي هو التطبيق الذي يبدأ مكالمة غير حظر ، ويمكنه التعامل مباشرة مع المهمة التالية دون الاقتراع ، ما عليك سوى تمرير البيانات إلى التطبيق من خلال إشارة أو رد اتصال بعد اكتمال I/O.
في الواقع ، يحتوي I/O غير المتزامن على تطبيقات مختلفة تحت أنظمة التشغيل المختلفة. على سبيل المثال ، يعتمد *NIX Platform تجمع مؤشرات ترابط مخصص ، بينما يعتمد Windows Platform نموذج IOCP. توفر Node Libuv كطبقة تغليف مجردة لتغليف الأحكام التوافق مع النظام الأساسي ، وتضمن أن يكون تنفيذ I/O غير المتزامن للعقدة العلوية والمنصات السفلية مستقلة. يجب التأكيد على أننا غالبًا ما نذكر أن العقدة ذات الخيوط الواحدة ، مما يعني فقط أن تنفيذ JavaScript في مؤشر ترابط واحد ، وهناك تجمعات مؤشرات ترابط أخرى تكمل مهام I/O بالفعل داخل العقدة.
2. عقدة غير متزامنة I/O
2.1 حلقة الحدث
نموذج تنفيذ العقدة هو في الواقع حلقة حدث. عندما تبدأ العملية ، تقوم العقدة بإنشاء حلقة لا حصر لها ، وتصبح كل عملية لتنفيذ هيئة الحلقة علامة. تتمثل كل عملية علامة في التحقق مما إذا كانت هناك أحداث تنتظر معالجتها. إذا كان الأمر كذلك ، فسيتم إزالة الأحداث ووظائف رد الاتصال ذات الصلة. إذا كانت هناك وظائف رد اتصال مرتبطة ، فسيتم تنفيذها ، ثم سيتم إدخال الحلقة التالية. إذا لم يكن هناك مزيد من معالجة الأحداث ، فاخرج من العملية.
2.2 المراقب
هناك العديد من المراقبين في كل حلقة حدث ، ومن خلال سؤال هؤلاء المراقبين ، يمكننا تحديد ما إذا كانت هناك أحداث يجب معالجتها. حلقة الحدث هي نموذج منتج/مستهلك نموذجي. في العقدة ، تأتي الأحداث بشكل أساسي من طلبات الشبكة ، وملف الإدخال/الإخراج ، وما إلى ذلك. تحتوي هذه الأحداث على مراقبي الإدخال/الإخراج في الشبكة المقابلة ، ومراقب الإدخال/الإخراج ، وما إلى ذلك.
2.3 طلب كائن
أثناء الانتقال من JavaScript إلى kernel الذي يقوم بعمليات الإدخال/الإخراج ، يوجد منتج وسيط يسمى كائن الطلب. أخذ أبسط طريقة لـ Fs.Open () في Windows (افتح ملفًا واحصل على واصف ملف وفقًا للمسار والمعلمات المحددة) كمثال ، من مكالمات JS إلى الوحدات النمطية المدمجة ، تسمى النظام من خلال libuv بالفعل طريقة UV_FS_OPEN (). أثناء عملية الاتصال ، يتم إنشاء كائن طلب FSREQWRAP ، ويتم تغليف المعلمات والأساليب التي تم تمريرها من طبقة JS في كائن الطلب هذا. يتم تعيين وظيفة رد الاتصال التي نشعر بالقلق أكثر على خاصية oncompete_sym لهذا الكائن. بعد لف الكائن ، ادفع كائن FSREQWRAP إلى تجمع مؤشرات الترابط وانتظر التنفيذ.
في هذه المرحلة ، يعود مكالمة JS على الفور ، ويمكن أن يستمر مؤشر ترابط JS في إجراء العمليات اللاحقة. تنتظر عملية الإدخال/الإخراج الحالية التنفيذ في مجموعة مؤشرات الترابط ، والتي تكمل المرحلة الأولى من المكالمة غير المتزامنة.
2.4 تنفيذ عمليات الاسترداد
إشعار رد الاتصال هو المرحلة الثانية من I/O غير المتزامن. بعد استدعاء عملية الإدخال/الإخراج في تجمع الخيوط ، سيتم تخزين النتائج التي تم الحصول عليها ، ثم يتم إخطار IOCP بأن عملية الكائن الحالية قد اكتملت وإرجاع سلسلة الرسائل. أثناء كل تنفيذ علامة ، سيتصل مراقب الإدخال/الإخراج بحلقة الحدث بالطريقة ذات الصلة للتحقق مما إذا كانت هناك طلبات مكتملة في مجموعة مؤشرات الترابط. إذا كان موجودًا ، فسيتم إضافة كائن الطلب إلى قائمة انتظار I/O Observer ومن ثم معالجتها كحدث.
3. API غير متزامن
هناك أيضًا بعض واجهات برمجة التطبيقات غير المتزامنة غير المرتبطة بـ I/O في العقدة ، مثل Timers SetTimeOut () ، setInterval () ، process.nexttick () و setimmdiate () التي تنفذ المهام على الفور بشكل غير متزامن ، وما إلى ذلك ، والتي سيتم تقديمها لفترة وجيزة هنا.
3.1 Timer API
واجهات برمجة التطبيقات على جانب المتصفح من setTimeout () و setInterval () متسقة. يشبه مبدأ التنفيذ الخاص بهم I/O غير المتزامن ، لكنهم لا يتطلبون مشاركة تجمع مؤشرات الترابط I/O. سيتم إدراج المؤقت الذي تم إنشاؤه عن طريق استدعاء واجهة برمجة تطبيقات المؤقت في شجرة حمراء وسوداء داخل مراقب الموقت. سوف تكرر علامة كل حلقة حدث الكائن الموقت من الشجرة الحمراء والأسود للتحقق مما إذا كان الوقت قد تجاوز الوقت. إذا تجاوز ذلك ، سيتم تشكيل حدث وسيتم تنفيذ وظيفة رد الاتصال على الفور. المشكلة الرئيسية في المؤقت هي أن وقت توقيته ليس دقيقًا بشكل خاص (ميلي ثانية ، ضمن التسامح).
3.2 واجهة برمجة تطبيقات تنفيذ المهام غير المتزامنة
قبل ظهور العقدة ، قد يتصل الكثير من الناس بهذا من أجل أداء مهمة على الفور بشكل غير متزامن:
نسخة الكود كما يلي:
setTimeout (function () {
// تودو
} ، 0) ؛
نظرًا لخصائص حلقات الأحداث ، فإن المؤقت ليس دقيقًا بما فيه الكفاية ، ويتطلب استخدام شجرة حمراء وسوداء استخدام مؤقت ، وتعقيد وقت التشغيل مختلف هو O (السجل (N)). ستضع طريقة Process.NextTick () وظيفة رد الاتصال فقط في قائمة الانتظار وتخرجها وتنفيذها في الجولة التالية من القراد. التعقيد هو O (1) وهو أكثر كفاءة.
بالإضافة إلى ذلك ، هناك طريقة setimmediate () مماثلة للطريقة أعلاه ، كلاهما تأخير تنفيذ وظيفة رد الاتصال. ومع ذلك ، فإن الأول له أولوية أعلى من الأخير ، لأن حلقة الحدث تتحقق من المراقب بالتسلسل. بالإضافة إلى ذلك ، يتم حفظ وظيفة رد الاتصال السابق في صفيف ، وسيقوم كل جولة من القراد بتنفيذ جميع وظائف رد الاتصال في المصفوفة ؛ يتم حفظ النتيجة الأخيرة في قائمة مرتبطة ، وسيقوم كل جولة من القراد بتنفيذ وظيفة رد الاتصال فقط.
4. خوادم تعتمد على الحدث وعالي الأداء
يوضح المثال السابق كيف تنفذ العقدة I/O غير متزامن. في الواقع ، تطبق Node أيضًا I/O غير متزامن لمعالجة مقبس الشبكة ، وهو أيضًا أساس Node لإنشاء خادم ويب. نماذج الخادم الكلاسيكية هي:
1. متزامن: يمكن معالجة طلب واحد فقط في وقت واحد ، وبقية الطلبات في حالة انتظار
2. لكل عملية/طلب لكل طلب: ابدأ عملية واحدة لكل طلب ، ولكن موارد النظام محدودة وليس لديها قابلية للتوسع.
3. لكل موضوع/لكل طلب: ابدأ مؤشر ترابط واحد لكل طلب. المواضيع أخف من العمليات ، لكن كل مؤشر ترابط يحتل قدرًا معينًا من الذاكرة. عند وصول الطلبات المتزامنة الكبيرة ، ستنفد الذاكرة قريبًا.
يتبنى Apache الشهير نموذج لكل خيوط/لكل نسبة ، وهذا هو السبب في أنه من الصعب التعامل مع التزامن العالي. تتعامل العقدة مع الطلبات من خلال الأساليب التي تعتمد على الحدث ، والتي يمكن أن تنقذ النفقات العامة لإنشاء وتدمير الخيوط. في الوقت نفسه ، يحتوي نظام التشغيل على عدد أقل من مؤشرات الترابط عند جدولة المهام ، كما أن تكلفة تبديل السياق منخفضة للغاية. يمكن للعقدة التعامل مع الطلبات بطريقة منظمة حتى مع وجود عدد كبير من الاتصالات.
يتخلى الخادم المعروف NGINX أيضًا عن طريقة الخيوط المتعددة ويعتمد نفس الطريقة التي تعتمد على الحدث مثل العقدة. الآن Nginx هو بطريقة كبيرة لاستبدال أباتشي. Nginx مكتوب في Pure C وله أداء عالي ، ولكنه مناسب فقط لخوادم الويب ، وتستخدم للوكالة العكسية أو موازنة التحميل ، وما إلى ذلك. يمكن للعقدة بناء نفس الوظائف مثل Nginx ، ويمكنها أيضًا التعامل مع العديد من الشركات المحددة ، كما أن أدائها جيدًا. في المشاريع الفعلية ، يمكننا الجمع بينها لتحقيق أفضل أداء للتطبيق.