8.5. การหมดเวลาและการยกเลิก

การยกเลิก (Cancellation) คือชื่อที่ asyncio ใช้สำหรับการ "หยุดรัน coroutine นี้ ยกข้อยกเว้นภายใน coroutine เพื่อให้มีโอกาสล้างข้อมูล และลบออกจาก scheduler" การหมดเวลาเป็นเหตุผลที่พบบ่อยที่สุดในการยกเลิกบางอย่าง ส่วนอีกวิธีคือการใช้ task.cancel() ด้วยตนเอง

8.5.1. การยกเลิก task

การเรียก Task.cancel จะกำหนดเวลาให้ asyncio.CancelledError ถูกยกขึ้นภายใน coroutine ที่กำลังทำงานที่จุด await ถัดไป coroutine สามารถเลือกที่จะไม่สนใจการยกเลิก (จับข้อยกเว้นและทำงานต่อไป) หรือจะยอมรับมัน -- ตัวเลือกปกติคือยอมรับหลังจากรันโค้ดล้างข้อมูลแล้ว:

async def network_worker(stream):
    try:
        while True:
            data = await stream.read(64)
            # ...
    finally:
        stream.close()

คำสั่ง finally แบบเปล่าเป็นรูปแบบที่สะอาดที่สุด: การล้างข้อมูลจะทำงานไม่ว่า coroutine จะออกปกติ ยกข้อยกเว้นที่ไม่เกี่ยวข้อง หรือถูกยกเลิก CancelledError จะกระจายกลับผ่าน finally ลูปเห็นว่า task เสร็จแล้ว และผู้เรียก await task เห็นการยกเลิก

การจับ CancelledError อย่างชัดเจนก็ใช้ได้เช่นกัน เมื่อแอปพลิเคชันต้องการทำบางอย่างโดยเฉพาะกับมัน -- บันทึก ส่งมอบทรัพยากรอย่างสะอาด เป็นต้น กฎคือ: ยก re-raise หลังจากโค้ดล้างข้อมูลทำงาน การกลืน CancelledError ทำให้ coroutine ยังมีชีวิตอยู่เมื่อผู้เรียกขอให้หยุด ซึ่งเกือบทุกครั้งเป็นข้อผิดพลาด:

async def worker():
    try:
        await long_running_thing()
    except asyncio.CancelledError:
        log("worker cancelled, cleaning up")
        close_resources()
        raise          # << this line is the important one

8.5.2. การหมดเวลาด้วย wait_for

asyncio.wait_for() ห่อ awaitable ด้วย deadline หาก awaitable เสร็จสิ้นภายในเวลาที่กำหนด ผลลัพธ์จะถูกส่งคืน หากไม่เสร็จ awaitable จะถูกยกเลิกและผู้เรียกจะได้รับ asyncio.TimeoutError

try:
    frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
    print("camera took too long")

อาร์กิวเมนต์ seconds รับค่า float สำหรับ deadline ที่มีหน่วยเป็นมิลลิวินาที asyncio.wait_for_ms() รับจำนวนมิลลิวินาทีเป็น integer -- ซึ่งเป็น extension ของ MicroPython ที่สอดคล้องกับ timing knobs ที่มีหน่วยเป็นมิลลิวินาทีของเฟิร์มแวร์

ภายใน wait_for ทำงานเหมือนกับการยกเลิกด้วยตนเอง: เมื่อ deadline หมดเวลา มันจะเรียก cancel() บน task ที่ห่อไว้ CancelledError จะถูกยกขึ้นภายใน coroutine คำสั่ง finally จะทำงาน และเมื่อการล้างข้อมูลเสร็จสิ้น ข้อยกเว้นจะถูกแปลเป็น TimeoutError สำหรับผู้เรียก

นั่นหมายความว่า coroutine ที่จับและไม่สนใจ CancelledError จะ ทำให้การหมดเวลาล้มเหลว -- deadline หมดแล้ว แต่ coroutine ปฏิเสธที่จะหยุด และ wait_for ไม่สามารถบังคับได้ กฎเดิมใช้ที่นี่ด้วย: จับ CancelledError เฉพาะเพื่อล้างข้อมูล แล้ว re-raise

8.5.3. การยกเลิกผ่าน gather

การยกเลิกจะกระจายลงผ่าน gather() หาก task ที่กำลัง await การเรียก gather ถูกยกเลิก awaitable ทุกตัวที่ยังทำงานอยู่ภายใน gather ก็จะถูกยกเลิกด้วย -- แต่ละตัวมีโอกาสล้างข้อมูลผ่านคำสั่ง finally ของตัวเองก่อนที่การยกเลิกจะส่งกลับไปยังผู้เรียก

ร่วมกับการหมดเวลา นี่คือวิธีมาตรฐานในการกำหนด deadline สำหรับ กลุ่ม ของการดำเนินการ:

await asyncio.wait_for(
    asyncio.gather(a(), b(), c()),
    timeout=5,
)

ไม่ว่า sub-operation ทุกตัวจะเสร็จภายในห้าวินาที หรือทั้งหมดจะถูกยกเลิกพร้อมกัน

8.5.4. การปิดแอปพลิเคชัน

การยกเลิกยังเป็นวิธีที่แอปพลิเคชันจริงหยุดอย่างสะอาด รูปแบบสอดคล้องกันในสคริปต์ต่างๆ: main จับ handle ของ background task ที่รันอยู่นาน ทำงานระดับบนสุด จากนั้นยกเลิก handle แต่ละอันและ await มันในบล็อก finally

async def main():
    sender = asyncio.create_task(uplink())
    watcher = asyncio.create_task(button_watcher())
    try:
        await snapshot_loop()
    finally:
        sender.cancel()
        watcher.cancel()
        await asyncio.gather(sender, watcher,
                             return_exceptions=True)

return_exceptions=True คือเทคนิคที่ป้องกันไม่ให้ gather ยก re-raise CancelledError ที่ child task แต่ละตัวกำลังจะส่งมา ดังนั้นเหตุผลในการออกของแอปพลิเคชันเอง -- ไม่ว่า snapshot_loop จะยกหรือไม่ยก -- จะเป็นสิ่งที่ลอยออกมาจาก main