8.15. ข้อผิดพลาดที่พบบ่อย¶
รูปแบบที่ทำให้ asyncio ดีนั้น -- ไม่มีการถูกขัดจังหวะ, await แบบชัดเจน -- ก็ทำให้เกิดปัญหาเฉพาะตัวเช่นกัน หน้านี้รวบรวมปัญหาที่พบบ่อยพอที่ควรรู้ไว้
8.15.1. ลืม await¶
การเรียกฟังก์ชัน async def จะคืนค่า coroutine object ไม่ใช่การรันเนื้อหาของฟังก์ชัน หากต้องการให้รันจริง coroutine ต้องถูก 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
ข้อผิดพลาดนี้เงียบ -- coroutine object ถูกสร้าง, ถูกทิ้ง, และไม่เคยถูกรันเลย แอปพลิเคชันดำเนินต่อเหมือนทุกอย่างทำงานปกติ MicroPython บางครั้งจะบันทึกคำเตือนว่า coroutine ไม่เคยถูก await; บางครั้งก็ไม่แจ้ง ตรวจสอบตำแหน่งที่ขาด await ทุกจุดที่เรียกใช้ฟังก์ชัน
8.15.2. ลูปแน่นโดยไม่มี await¶
coroutine ที่ทำงานในลูปโดยไม่มี await จะครอบครอง event loop ไว้ ไม่มีงานอื่นที่จะคืบหน้าได้จนกว่าลูปจะออกหรือยอมส่งการควบคุม:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
วิธีแก้คือการ yield ภายในลูป -- มักใช้ await asyncio.sleep_ms(0) -- เพื่อให้งานที่พร้อมทำงานอื่นได้มีโอกาสทำงาน งานที่ต้องคำนวณหนักควรอยู่ในรูปแบบนี้ด้วย: ลูปประมวลผลภาพที่ทำงานนานหลายร้อยมิลลิวินาทีต่อรอบควร yield อย่างน้อยหนึ่งครั้งต่อรอบ เพื่อไม่ให้ส่วนอื่นของโปรแกรมหยุดชะงัก
8.15.3. กลืน CancelledError¶
หน้า การยกเลิก ได้กล่าวถึงเรื่องนี้อย่างละเอียดแล้ว กล่าวซ้ำที่นี่เพราะเป็นสาเหตุที่พบบ่อยที่สุดของปัญหา "แอปพลิเคชันของฉันไม่สามารถปิดได้": coroutine จับ asyncio.CancelledError เพื่อทำความสะอาดแต่ลืม re-raise มัน งานยังคงทำงานต่อ และ caller ที่ขอยกเลิกจะค้างไปตลอดกาลรอให้มันเสร็จ ให้ re-raise เสมอหลังทำความสะอาด หรือใช้บล็อก try/finally แทน except แบบชัดเจน
8.15.5. await ระดับโมดูล¶
await ใช้ได้เฉพาะภายใน async def เท่านั้น การเขียนที่ระดับโมดูล -- ภายนอก coroutine ใดๆ -- เป็น syntax error:
# bug: not inside an async def
result = await fetch()
วิธีแก้คือนำงานไปไว้ใน coroutine แล้วเรียกจาก entry point asyncio.run() ของโปรแกรม
8.15.6. การเรียก asyncio.run หลายครั้ง¶
MicroPython มี event loop หนึ่งเดียว การเรียก asyncio.run() สองครั้งติดกัน -- ครั้งแรกสำหรับตั้งค่า, ครั้งที่สองสำหรับงานหลัก -- ยังคงใช้ loop เดิม การเรียกมัน จากภายใน coroutine ที่กำลังทำงานเป็น error: loop กำลังทำงานอยู่แล้ว ทั้งสองกรณีนี้เกิดบ่อยที่สุดเมื่อสคริปต์เติบโตขึ้นเรื่อยๆ และผู้เขียนพยายามขยายโดยเพิ่มการเรียก run() เพิ่มเติมแทนที่จะรวมงานใหม่เข้ากับ main ที่มีอยู่
8.15.7. การใช้ Event จาก interrupt¶
asyncio.Event.set() ปลอดภัยที่จะเรียกเฉพาะจากภายใน event loop เท่านั้น การเรียกจาก GPIO interrupt handler เป็นความเสี่ยงต่อการทำข้อมูลเสียหาย สำหรับการปลุกงานจาก interrupt ให้ใช้ ThreadSafeFlag แทน -- หน้า เกี่ยวกับมัน ครอบคลุมรูปแบบการใช้งาน
8.15.8. การเรียกใช้แบบ synchronous ที่ใช้เวลานาน¶
coroutine สามารถ await primitive การรอของ asyncio เองได้; สิ่งอื่นใดที่มันเรียกจะทำงานแบบ synchronous และบล็อก loop จนกว่าจะคืนค่า การ time.sleep() แบบ blocking 200 ms, การเขียน SD card ที่ใช้เวลา 80 ms ในการ flush, การบีบอัด JPEG ขนาดใหญ่, การเรียก csi.CSI.snapshot() -- แต่ละอย่างเหล่านี้ถือครอง event loop ตลอดระยะเวลาทั้งหมด วิธีแก้ขึ้นอยู่กับการเรียกนั้น:
สำหรับ
time.sleep: แทนที่ด้วยawait asyncio.sleepหรือawait asyncio.sleep_msสำหรับ
csi.CSI.snapshot: ใช้ async snapshot wrapper ที่หน้าการจับภาพสร้างขึ้นสำหรับการคำนวณที่ใช้เวลานาน (การประมวลผลภาพ, การเข้ารหัส JPEG): ยอมรับค่าใช้จ่าย หรือแบ่งงานออกเป็นส่วนที่
awaitระหว่างรอบการทำงาน
Asyncio ไม่สามารถทำให้การเรียกแบบ synchronous กลายเป็น non-blocking ได้ มันสามารถแค่ให้ coroutine อื่นทำงาน ในขณะที่ บางอย่างกำลัง await อยู่เท่านั้น