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.4. การเปลี่ยนแปลงสถานะที่ใช้ร่วมกันข้ามการ await

การ scheduling แบบ cooperative รับประกันว่า coroutine จะมี CPU เป็นของตัวเอง ระหว่าง การ await แต่ทันทีที่มัน await coroutine อื่นสามารถทำงานได้ ถ้าสอง coroutine แก้ไขโครงสร้างข้อมูลเดียวกันในขั้นตอนที่มี await การดำเนินการของพวกมันอาจสลับกันในแบบที่ทำให้โครงสร้างเสียหาย:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

สำหรับสถานะที่ เปลี่ยนแปลงเฉพาะระหว่าง await ภายใน coroutine เดียว ไม่จำเป็นต้องซิงโครไนซ์ สำหรับสถานะที่เปลี่ยนแปลงข้ามการ await และเข้าถึงจากหลาย coroutine ให้ห่อ critical section ใน Lock

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 อยู่เท่านั้น