8.5. Hết thời gian chờ và hủy tác vụ¶
Hủy tác vụ (cancellation) là cách asyncio nói "dừng coroutine này lại, ném một ngoại lệ vào bên trong nó để nó có cơ hội dọn dẹp, rồi xóa nó khỏi bộ lập lịch". Hết thời gian chờ (timeout) là lý do phổ biến nhất để hủy; task.cancel() thủ công là lý do còn lại.
8.5.1. Hủy một tác vụ¶
Gọi Task.cancel sẽ lên lịch để asyncio.CancelledError được ném vào bên trong coroutine đang chạy tại điểm await tiếp theo. Coroutine có thể bỏ qua việc hủy (bắt ngoại lệ và tiếp tục chạy) hoặc chấp nhận nó -- lựa chọn thông thường là chấp nhận sau khi chạy mã dọn dẹp:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Mệnh đề finally đơn thuần là mẫu gọn nhất: mã dọn dẹp sẽ chạy dù coroutine thoát bình thường, ném một ngoại lệ không liên quan, hay bị hủy. CancelledError lan truyền ngược qua finally, vòng lặp nhận thấy tác vụ đã hoàn thành, và lệnh gọi await task sẽ nhận được thông báo hủy.
Bắt CancelledError một cách tường minh cũng được khi ứng dụng muốn xử lý cụ thể -- ghi log, giải phóng tài nguyên gọn gàng, v.v. Quy tắc là: ném lại ngoại lệ sau khi mã dọn dẹp chạy xong. Nuốt CancelledError khiến coroutine tiếp tục sống khi người gọi đã yêu cầu dừng, điều đó hầu như luôn là lỗi:
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. Hết thời gian chờ với wait_for¶
asyncio.wait_for() bọc một awaitable trong một thời hạn. Nếu awaitable hoàn thành trong thời gian chờ, kết quả của nó được trả về. Nếu không, awaitable bị hủy và người gọi nhận asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Đối số seconds chấp nhận kiểu float. Đối với thời hạn theo mili-giây, asyncio.wait_for_ms() nhận một số nguyên mili-giây -- một phần mở rộng MicroPython phù hợp với các nút định thời gian theo mili-giây của firmware.
Về mặt nội bộ, wait_for thực hiện chính xác những gì hủy thủ công sẽ làm: khi thời hạn hết, nó gọi cancel() trên tác vụ được bọc, CancelledError được ném vào bên trong coroutine, các mệnh đề finally chạy, và khi quá trình dọn dẹp hoàn tất, ngoại lệ được chuyển thành TimeoutError cho người gọi.
Điều đó có nghĩa là một coroutine bắt và bỏ qua CancelledError sẽ vô hiệu hóa thời gian chờ -- thời hạn đã hết nhưng coroutine từ chối dừng, và wait_for không thể ép buộc. Quy tắc trước đó áp dụng ở đây cũng vậy: chỉ bắt CancelledError để chạy mã dọn dẹp, rồi ném lại.
8.5.3. Hủy qua gather¶
Hủy lan truyền xuống thông qua gather(). Nếu tác vụ đang chờ một lời gọi gather bị hủy, mọi awaitable vẫn đang chạy bên trong gather cũng bị hủy -- mỗi cái đều có cơ hội dọn dẹp qua các mệnh đề finally trước khi việc hủy lan ngược lên người gọi.
Kết hợp với thời gian chờ, đây là cách tiêu chuẩn để đặt thời hạn cho một nhóm hoạt động:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Hoặc mọi thao tác con đều hoàn thành trong năm giây, hoặc tất cả đều bị hủy cùng nhau.
8.5.4. Tắt ứng dụng¶
Hủy cũng là cách một ứng dụng thực sự dừng lại gọn gàng. Mẫu nhất quán trong các tập lệnh: main nắm giữ các tham chiếu đến các tác vụ nền có thời gian sống dài mà nó đã khởi động, thực hiện công việc cấp cao nhất, rồi hủy từng tham chiếu và await nó trong một khối 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 là thủ thuật giúp gather không ném lại CancelledError mà mỗi tác vụ con sắp chuyển giao, nhờ đó lý do thoát của chính ứng dụng -- bất kể snapshot_loop có ném hay không -- mới là thứ nổi lên từ main.