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