عبدالفتاح الصلوي

مدخل إلى البرمجة غير المتزامنة Asynchronous Programming في JS

كتبها : عبدالفتاح الصلوي / في رصيف : البرمجة

مدخل إلى البرمجة غير المتزامنة Asynchronous Programming في JS

جافا سكربت (JavaScript) هي لغة برمجة غير متزامنة Asynchronous ، أحادية الخط single threaded ، معتمدة على الأحداث event driven .

قبل الولوج الى تفاصيل اللاتزامن دعونا نعرج قليلاً الى مفهومي أحادية الخط والإعتماد على الأحداث.

ماذا يقصد باحادية الخط  single threaded ؟

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


ماذا يعني الاعتماد على الأحداث event driven؟

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


مصطلحي التزامن واللاتزامن   Sync vs Async


مصطلح "Synchronous" مشتق من الجذر اليوناني Syn، الذي يعني “with” أو "مع"، وChronos، الذي يعني "الوقت". لذا فكلمة "Synchronous" تعني حرفيًا "مع الوقت". لذا فهو يشير الى تنفيذ الكود في نفس الوقت، حيث  يتم تشغيل  التعليمات البرمجية واحدًا تلو الآخر ولا يبدأ السطر التالي إلا بعد معالجة السطر السابق والحصول على ناتج التنفيذ.
أما  Asynchronous فهو مشتق من الجذر اليوناني async، الذي يعني "ليس مع" ، ومن ثم فإن Asynchronous يعني حرفيًا "ليس مع الوقت". لذا فهو يشير الى تنفيذ التعليمات البرمجية دون مراعاة وقت حدوثها. فقد لا يتم انتظار الانتهاء من تنفيذ الكود في السطر الحالي في أول مرة يقرأ فيه مترجم اللغة التعليمات البرمجية.
 

وقت التزامن واللاتزامن   Asynchronous vs  Synchronous  timing في جافا سكربت


هناك نوعان من التعليمات البرمجية في جافا سكربت (Synchronous و Asynchronous) .
 في الكود الغير متزامن (Asynchronous JavaScript)، يتعامل محرك اللغة مع الأكواد البطيئة والسريعة بشكل مختلف.

نحن نعرف معنى الكلمات "سريع" و "بطيء"، كيف يتم تطبيق ذلك عمليًا على الأكواد الخاصة بنا؟
   كما وضحنا بأن JS احادية الخيط (single threaded) ولكن عند تنفيذ الاكواد اللامتزامنة (Asynchronous) تتيح للخيط (Thread) تنفيذ أسطر جديدة من الكود أثناء انتظاره للرد من عملية بطيئة تعتمد على الوقت، مثل عمليات التعامل مع نظام الملفات إدخال/إخراج (File System I/O).
لفهم ذلك، يجب أن نفهم قليلاً  سرعات تشغيل الكمبيوتر. فوحدات المعالجة المركزية (CPUs) سريعة جدًا ويمكنها التعامل مع الملايين إلى المليارات من العمليات في الثانية الواحدة. لكن أجزاء أخرى من الكمبيوتر أو الشبكة تعمل بسرعة أبطأ من وحدة المعالجة المركزية. على سبيل المثال، القرص الصلب (Hard Drive)  يقوم فقط بمئات إلى آلاف العمليات في الثانية، بينما قد تقوم شبكة الكمبيوتر بعملية واحدة فقط في الثانية.

 لذا فاستدعاء الذاكرة يكون أبطأ بكثير من دورة المعالجة الحاسوبية. وعمليات القرص الصلب أبطأ بعدة درجات من عمليات الذاكرة. استدعاءات الشبكة أبطأ بعدة درجات من استدعاءات القرص الصلب.
 (يقوم محرك لغة Javascript  بالتعامل مع التعليمات البرمجية البطيئة والسريعة بشكل مختلف.)


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

بشكل عام، في الأكواد الغير متزامنة (Asynchronous code)، يتم استخدام مفهوم المهمة (Task) أو الوعد (Promise) أو تعاقب العمليات (Callback) للتعامل مع العمليات البطيئة. يقوم محرك الجافاسكريبت بإرسال العملية البطيئة إلى الخلفية ويستمر في تنفيذ الأكواد الأخرى دون الانتظار لاستكمال العملية البطيئة. عند اكتمال العملية البطيئة، يتم تنفيذ دوال (Callback) أو استدعاء الوعد (Promise) لمعالجة النتيجة.

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

مخطط توقيت المزامنة sync مقابل عدم التزامن  async
مخطط توقيت المزامنة Sync مقابل عدم التزامن Async  


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

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

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

* المراجع : كتاب Advanced JavaScript ل Zachary Shute

علامات ذات صلة :