2.32. الإجراءات المتزامنة (Coroutines)

الإجراء المتزامن (coroutine) هو دالة يمكنها التوقف مؤقتًا في منتصف تنفيذها ثم استئناف العمل لاحقًا من حيث توقفت، مع بقاء جميع متغيراتها المحلية سليمة. وهو يعكس نمط المولّد (generator) -- جسم دالة يمكن تعليقه واستئنافه -- مع تغيير واحد في الجهة التي تقود كل استئناف.

صيغة Python لكتابة إجراء متزامن هي زوج الكلمتين المفتاحيتين async / await. تشير async إلى أن الدالة إجراء متزامن؛ بينما تشير await إلى النقاط داخلها التي يُسمح فيها بالتوقف المؤقت.

2.32.1. تعريف إجراء متزامن

الدالة المعرّفة باستخدام async def هي دالة إجراء متزامن. استدعاؤها لا يشغّل جسمها؛ بل يُعيد كائن إجراء متزامن لم يبدأ بعد:

async def greet(name):
    print("hello,", name)

coro = greet("Alice")        # body NOT run yet

كائن الإجراء المتزامن متوقف مؤقتًا عند بداية الدالة تمامًا. يجب أن يقوم شيء ما بقيادته لتشغيل جسمه -- وهذا الشيء هو حلقة الأحداث (event loop)، وهي مكوّن من مكوّنات وقت التشغيل. يوفّر MicroPython واحدة في الوحدة asyncio. في الوقت الحالي، اعتبر الإجراء المتزامن "جاهزًا للتشغيل، في انتظار قائد".

2.32.2. التوقف المؤقت داخل إجراء متزامن

داخل إجراء متزامن، يُعلّق تعبير await التنفيذ حتى تصبح القيمة المنتظَرة جاهزة:

async def fetch_and_log():
    data = await read_sensor()
    print("got:", data)

عندما يصل الجسم إلى await read_sensor()، يُعيد الإجراء المتزامن التحكم إلى أيًّا كان ما يشغّله. وعندما تنتهي read_sensor()، يستأنف القائد الإجراء المتزامن عند السطر التالي، مع ربط النتيجة بـ data.

تكون await صالحة فقط داخل إجراء متزامن. واستخدامها في دالة عادية يُعد خطأً نحويًا.

2.32.3. العلاقة بالمولّدات

تشترك الإجراءات المتزامنة والمولّدات في الآلية الأساسية نفسها. الفرق هو في الجهة التي تسحب كل استئناف:

  • يُنتج المولّد قيمًا؛ ويسحب المستهلك القيمة التالية باستخدام next() أو عن طريق التكرار.

  • يُنتج الإجراء المتزامن تحكمًا؛ وتجدول حلقة الأحداث الاستئناف عندما تصبح العملية المنتظَرة جاهزة.

إذا كانت مصافحة إنتاج القيم في المولّد مفهومة، فإن مصافحة الإجراء المتزامن هي الفكرة نفسها -- لكنها مقادة بحلقة أحداث بدلًا من حلقة for.

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

في الخفاء، تستخدم await و yield ميزة وقت تشغيل Python نفسها لتعليق دالة واستئنافها. وتختلف الكلمتان المفتاحيتان لأن العُرف المحيط بهما يختلف: تُعيد yield قيمة إلى مستهلك يسحب باستخدام next()؛ بينما تُسلّم await التحكم إلى حلقة أحداث تجدول الاستئناف عندما تصبح العملية المنتظَرة جاهزة. ويُعد async / await في جوهره صيغة أحدث لنمط الإجراءات المتزامنة -- فقد بنت المكتبات الأقدم الإجراءات المتزامنة فوق آلية المولّدات مباشرة، باستخدام yield from (المقدَّمة في المُكرِّرات والمولّدات) لتفويض التعليق بين الإجراءات المتزامنة.

2.32.4. الإجراءات المتزامنة تحتاج إلى قائد

الإجراء المتزامن خامل بلا وقت تشغيل يقوده. تعريف واحد منها لا بأس به؛ لكن تشغيل واحد يحتاج إلى حلقة أحداث. وتوفّر وحدة asyncio في MicroPython حلقة الأحداث تلك. يغطي قسم Asyncio كيفية بدء الحلقة، وجدولة الإجراءات المتزامنة عليها، ومشاركة الحالة بينها باستخدام الأقفال والأحداث، ومعالجة الإلغاء والمهل الزمنية، وتشكيل تطبيق حقيقي حول الكلمتين المفتاحيتين async / await المقدَّمتين هنا.