8.15. المزالق¶
إن الأنماط نفسها التي تجعل asyncio لطيفًا -- لا استباق، و await صريحة -- تمنحه مجموعته الخاصة من الأشكال التي تعضّ. هذه الصفحة هي فهرس الأشكال التي تظهر بشكل متكرر بما يكفي لتستحق المعرفة بها.
8.15.1. نسيان await¶
استدعاء دالة async def يعيد كائن كوروتين. وهو لا يشغّل جسم الدالة. ولتنفيذه فعليًا، يجب انتظار الكوروتين بـ await أو تغليفها في مهمة:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
العلة صامتة -- إذ يُنشأ كائن الكوروتين ويُهمَل ولا يُنفَّذ قط. ويمضي التطبيق كأن كل شيء قد نجح. سيسجّل MicroPython أحيانًا تحذيرًا بأن كوروتين لم يُنتظر قط؛ وأحيانًا لن يفعل. دقّق بحثًا عن await المفقودة في كل موضع استدعاء يبدو كاستدعاء دالة.
8.15.2. الحلقات المحكمة دون await¶
إن كوروتين تعمل في حلقة ولا تستخدم await أبدًا تحتكر حلقة الأحداث. ولا تحرز أي مهمة أخرى تقدّمًا حتى تخرج الحلقة أو تتنازل:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
الإصلاح هو تنازل داخل الحلقة -- عادةً await asyncio.sleep_ms(0) -- كي تحصل المهام الجاهزة الأخرى على فرصة للعمل. والعمل كثيف الحساب ينتمي إلى هذا الشكل أيضًا: فحلقة معالجة صور تعمل لمئات الميلي ثانية لكل تكرار ينبغي أن تتنازل مرة واحدة على الأقل لكل تكرار كي لا يتوقف باقي البرنامج.
8.15.3. ابتلاع CancelledError¶
غطّت صفحة الإلغاء هذا بالتفصيل بالفعل. ونكرره هنا لأنه السبب الأكثر شيوعًا لـ "تطبيقي لا يتوقف عن العمل": إذ تلتقط كوروتين asyncio.CancelledError لأغراض التنظيف وتنسى إعادة إطلاقه. فتستمر المهمة في العمل؛ ويتعلّق المستدعي الذي طلب الإلغاء إلى الأبد في انتظار انتهائها. أعد الإطلاق دائمًا بعد التنظيف، أو استخدم كتلة try/finally بدلًا من except صريحة.
8.15.5. await على مستوى الوحدة¶
await صالحة فقط داخل جسم async def. وكتابتها على مستوى الوحدة -- خارج أي كوروتين -- خطأ في الصياغة:
# bug: not inside an async def
result = await fetch()
الإصلاح هو وضع العمل في كوروتين واستدعاؤه من نقطة دخول البرنامج asyncio.run().
8.15.6. استدعاءات asyncio.run المتعددة¶
لدى MicroPython حلقة أحداث واحدة. واستدعاء asyncio.run() مرتين متتاليتين -- مرة للإعداد ومرة للعمل الرئيسي -- ما يزال يستخدم الحلقة نفسها. واستدعاؤها من داخل كوروتين قيد التشغيل خطأ: فالحلقة قيد التشغيل بالفعل. وتظهر كلتا الحالتين غالبًا عندما ينمو البرنامج النصي عضويًا ويحاول المؤلف توسيعه بإضافة المزيد من استدعاءات run() بدلًا من طي العمل الجديد في main الموجودة.
8.15.7. استخدام Event من مقاطعة¶
إن asyncio.Event.set() آمنة للاستدعاء فقط من داخل حلقة الأحداث. واستدعاؤها من معالج مقاطعة GPIO خطر إفساد. ولإيقاظ مهمة من مقاطعة، استخدم ThreadSafeFlag بدلًا منها -- وتغطي الصفحة الخاصة بها هذا الشكل.
8.15.8. الاستدعاءات المتزامنة الطويلة¶
يمكن للكوروتين أن تنتظر بدائيات الانتظار الخاصة بـ asyncio؛ أما أي شيء آخر تستدعيه فيعمل بشكل متزامن ويحجب الحلقة حتى يعود. إن time.sleep() حاجبة لمدة 200 مللي ثانية، أو كتابة على بطاقة SD تستغرق 80 مللي ثانية للتفريغ، أو ضغط JPEG كبير، أو استدعاء csi.CSI.snapshot() -- كل من هذه تحجز حلقة الأحداث طوال مدتها بالكامل. ويعتمد الإصلاح على الاستدعاء:
بالنسبة لـ
time.sleep: استبدلها بـawait asyncio.sleepأوawait asyncio.sleep_ms.بالنسبة لـ
csi.CSI.snapshot: استخدم غلاف اللقطة غير المتزامن الذي تبنيه صفحة الالتقاط.بالنسبة للحساب الطويل (معالجة الصور، أو ترميز JPEG): اقبل التكلفة أو قسّم العمل إلى قطع تستخدم
awaitبين التكرارات.
لا يستطيع asyncio جعل استدعاء متزامن غير حاجب. كل ما يستطيعه هو السماح لكوروتينات أخرى بالعمل بينما ينتظر شيء آخر.