8.1. التزامن التعاوني

نموذج الجدولة في asyncio هو نموذج تعاوني، وليس استباقيًا. هذا التمييز هو أهم نموذج ذهني مفرد يبني عليه بقية هذا القسم، لذا يستحق توضيحه قبل ظهور أي شيفرة.

8.1.1. الاستباقي مقابل التعاوني

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

لا يمكن للمجدول التعاوني أن يبدّل بين قطع الشيفرة إلا عند النقاط التي تعيد فيها القطعة قيد التشغيل التحكم صراحةً. في asyncio، تكون هذه النقاط عند كل await وعند كل استدعاء لروتين مشترك يُسلّم التحكم داخليًا (وأكثرها شيوعًا asyncio.sleep()). بين عمليتي await، ينفرد الروتين المشترك قيد التشغيل بوحدة المعالجة المركزية لنفسه.

تنتج عن ذلك نتيجتان:

  • الروتين المشترك الذي لا ينتظر (await) أبدًا لا يُوقَف أبدًا. إذا ظل روتين مشترك في حلقة ضيقة دون await بداخلها، فإنه يحتكر المجدول ولا يحرز أي شيء آخر تقدمًا. الحل هو await asyncio.sleep_ms(0) (أو أي استدعاء انتظار آخر) عند نقطة مناسبة في الحلقة.

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

8.1.2. الطبقات الثلاث

يُبنى كل برنامج نصي يستخدم asyncio من الطبقات الثلاث نفسها. تغطي الصفحتان التاليتان هذه الطبقات بالتفصيل؛ وهذه هي المسمّيات التي ينبغي تذكّرها أثناء قراءتهما.

  • الروتينات المشتركة -- دوال مُعلَنة بـ async def، كل منها وحدة عمل قائمة بذاتها تنتظر (await) حيث يكون ذلك مناسبًا. قدّمت نظرة Python العامة الكلمتين المفتاحيتين async/await؛ في asyncio هما الطريقة التي يُسلّم بها الروتين المشترك التحكم إلى المجدول.

  • المهام -- غلاف يضعه asyncio.create_task() حول روتين مشترك لجدولته بالتزامن مع الروتين الحالي. يُنشئ التطبيق عادةً حفنة من المهام للأعمال طويلة الأمد (حلقة اللقطات، عميل الشبكة، قارئ UART، ...).

  • حلقة الأحداث -- المحرّك الكامن أسفل كل شيء، الذي يتتبّع أي الروتينات المشتركة في حالة انتظار وأيها جاهز للتشغيل، مبدّلًا بين المهام عند كل await. لا يكتب التطبيق الحلقة؛ بل يسلّم روتينًا مشتركًا عالي المستوى إلى asyncio.run() وتقود الحلقة كل شيء من هناك.

عندما يُوصف التطبيق بهذه الطريقة -- بوصفه مجموعة صغيرة من الروتينات المشتركة تركّبها حلقة أحداث -- يصبح التزامن خاصيةً من خصائص شكل البرنامج، لا شيئًا على التطبيق أن يديره خطوة بخطوة.