8.2. الروتينات المشتركة والمهام

الروتينات المشتركة هي وحدة العمل التي يُبنى منها برنامج asyncio؛ والمهام هي الطريقة التي يشغّل بها التطبيق عدة روتينات مشتركة بالتزامن.

8.2.1. الروتينات المشتركة

الـ روتين المشترك هو دالة مُعلَنة بـ async def

import asyncio

async def heartbeat(interval_ms):
    while True:
        print("tick")
        await asyncio.sleep_ms(interval_ms)

يبدو جسم الدالة كدالة عادية، مع مكوّن إضافي واحد: await. حيثما يتعيّن على الروتين المشترك أن ينتظر شيئًا -- إيقافًا مؤقتًا، أو قراءة من الشبكة، أو حدثًا يُضبَط -- فإنه ينتظر بـ await تعبيرًا يعرف كيف يعلّق الروتين المشترك حتى يصبح الشيء الذي ينتظره جاهزًا. عند كل await يعيد الروتين المشترك التحكم إلى asyncio؛ ويستأنفه asyncio من النقطة نفسها بمجرد اكتمال العملية المنتظَرة.

تأتي وحدة asyncio مع نوعين من الإيقاف المؤقت:

  • asyncio.sleep() -- الوسيطة بالثواني، وتقبل عددًا عشريًا.

  • asyncio.sleep_ms() -- الوسيطة بالميلي ثانية، وتأخذ عددًا صحيحًا. وهي امتداد خاص بـ MicroPython؛ وعادةً ما تكون الخيار الصحيح على الكاميرا لأن مقابض التوقيت في البرنامج الثابت مصمّمة على أساس الميلي ثانية.

إن async def بمفردها لا تفعل شيئًا من تلقاء نفسها. فاستدعاء heartbeat(500) لا ينفّذ جسم الدالة؛ بل يعيد كائن روتين مشترك يتعيّن على asyncio جدولته. وأبسط طريقة لجدولة واحد هي asyncio.run()

asyncio.run(heartbeat(500))

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

8.2.2. المهام

الـ مهمة هي غلاف asyncio حول روتين مشترك يقول جدوِل هذا بالتزامن مع الروتين الحالي ودعني أتابع. تنشئ asyncio.create_task() واحدة وتعيد كائن Task يمثّل العمل المجدوَل:

task = asyncio.create_task(heartbeat("fast", 100))

أصبح الروتين المشترك الآن على جدول الحلقة؛ ولم ينتظره المستدعي. الكائن Task المُعاد هو المقبض الذي يستخدمه المستدعي بعد ذلك للتفاعل مع ذلك العمل قيد التشغيل.

بمجرد أن يحصل التطبيق على المقبض، يمكنه فعل ثلاثة أشياء به:

  • انتظار انتهاء المهمة. الكائن Task نفسه قابل للانتظار. فـ result = await task يعلّق الروتين المشترك الحالي حتى يعود روتين task المشترك، ثم يستأنف بأي شيء أعاده ذلك الروتين المشترك (أو يعيد إطلاق أي شيء أطلقه).

  • إلغاء المهمة. يجدول task.cancel() إطلاق asyncio.CancelledError داخل الروتين المشترك للمهمة عند await التالي لها، مانحًا إياها فرصة لتشغيل شيفرة التنظيف في كتلة finally. تغطي صفحة المهل الزمنية والإلغاء التفاصيل.

  • التعرّف عليها لاحقًا. تعيد asyncio.current_task() الكائن Task الخاص بالروتين المشترك قيد التشغيل حاليًا. لا تستدعيها معظم البرامج النصية أبدًا؛ فهي تظهر في القياس وفي معالجات الاستثناءات.

ليس على البرنامج النصي أن يلتقط المقبض في كل مرة. فالمهام الخلفية المؤقتة التي يبدأها التطبيق ويتركها قيد التشغيل يمكن أن تُسقِط القيمة المُعادة -- وما زالت الحلقة تجدولها:

import asyncio

async def heartbeat(name, interval_ms):
    while True:
        print(name)
        await asyncio.sleep_ms(interval_ms)

async def main():
    asyncio.create_task(heartbeat("fast", 100))
    asyncio.create_task(heartbeat("slow", 500))
    await asyncio.sleep(5)

asyncio.run(main())

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

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

8.2.3. قاعدة السطرين

أصغر برنامج asyncio هو السطران اللذان تنتهي بهما الأمثلة أعلاه:

async def main():
    ...

asyncio.run(main())

كل شيء آخر -- المهام التي ينشئها التطبيق، والأوّليات التي ينسّقها بها، والتدفقات التي يفتحها -- يحدث داخل main (وداخل الروتينات المشتركة التي تولّدها main). عندما يتجاوز برنامج نصي حلقة الكاميرا الكلاسيكية while True: csi0.snapshot()، فالحل ليس استدعاء asyncio.run() في عدة أماكن؛ بل دمج العمل الجديد في main بوصفه مزيدًا من المهام.