8.15. Những Bẫy Thường Gặp¶
Chính những đặc điểm làm cho asyncio trở nên tiện lợi -- không có preemption, phải dùng awaitmột cách rõ ràng -- cũng tạo ra một số bẫy đặc trưng của nó. Trang này là danh mục những bẫy xuất hiện đủ thường xuyên để đáng biết.
8.15.1. Quên await¶
Gọi một hàm async def sẽ trả về một đối tượng coroutine. Nó không chạy phần thân của hàm. Để thực sự thực thi nó, coroutine phải được awaited hoặc bọc trong một task:
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
Lỗi này không có thông báo -- đối tượng coroutine được tạo ra, bị loại bỏ và không bao giờ được thực thi. Ứng dụng tiếp tục như thể mọi thứ hoạt động bình thường. MicroPython đôi khi sẽ ghi một cảnh báo rằng một coroutine chưa bao giờ được await; đôi khi thì không. Hãy kiểm tra tất cả các vị trí gọi có vẻ như là lời gọi hàm để tìm awaitcòn thiếu.
8.15.2. Vòng lặp chặt chẽ không có await¶
Một coroutine chạy trong vòng lặp mà không bao giờ awaitchiếm độc quyền event loop. Không có task nào khác tiến lên cho đến khi vòng lặp thoát ra hoặc nhường:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
Cách sửa là thêm yield bên trong vòng lặp -- thường là await asyncio.sleep_ms(0) -- để các task khác đang sẵn sàng có cơ hội chạy. Công việc tính toán nặng cũng thuộc dạng này: một vòng lặp xử lý ảnh chạy hàng trăm mili giây mỗi lần lặp nên nhường ít nhất một lần mỗi lần lặp để phần còn lại của chương trình không bị đình trệ.
8.15.3. Nuốt CancelledError¶
Trang huỷ bỏ đã đề cập điều này chi tiết. Nhắc lại ở đây vì đây là nguyên nhân phổ biến nhất của "ứng dụng của tôi không tắt được": một coroutine bắt asyncio.CancelledError để dọn dẹp và quên re-raise lại. Task tiếp tục chạy; người gọi yêu cầu huỷ bỏ chờ mãi cho đến khi nó kết thúc. Luôn re-raise sau khi dọn dẹp, hoặc dùng khối try/finally thay vì except rõ ràng.
8.15.5. await ở cấp module¶
await chỉ hợp lệ bên trong phần thân async def. Viết nó ở cấp module -- ngoài bất kỳ coroutine nào -- là lỗi cú pháp:
# bug: not inside an async def
result = await fetch()
Cách sửa là đặt công việc vào một coroutine và gọi nó từ điểm vào asyncio.run() của chương trình.
8.15.6. Nhiều lần gọi asyncio.run¶
MicroPython có một event loop. Gọi asyncio.run() hai lần liên tiếp -- một lần để thiết lập, một lần cho công việc chính -- vẫn dùng cùng một loop. Gọi nó từ bên trong một coroutine đang chạy là lỗi: loop đã đang chạy. Cả hai trường hợp thường xảy ra nhất khi một tập lệnh phát triển dần và tác giả cố gắng mở rộng nó bằng cách thêm nhiều lần gọi run() thay vì gộp công việc mới vào main hiện có.
8.15.7. Sử dụng Event từ một ngắt¶
asyncio.Event.set() chỉ an toàn để gọi từ bên trong event loop. Gọi nó từ trình xử lý ngắt GPIO là nguy cơ gây hỏng dữ liệu. Để đánh thức một task từ ngắt, hãy dùng ThreadSafeFlag thay thế -- trang về nó trình bày cách dùng.
8.15.8. Các lời gọi đồng bộ dài¶
Một coroutine có thể await các nguyên thủy chờ của asyncio; mọi thứ khác mà nó gọi chạy đồng bộ và chặn loop cho đến khi nó trả về. time.sleep() chặn 200 ms, lệnh ghi thẻ SD mất 80 ms để flush, nén JPEG lớn, lời gọi csi.CSI.snapshot() -- mỗi lời gọi đó giữ event loop trong toàn bộ thời gian. Cách sửa phụ thuộc vào lời gọi:
Với
time.sleep: thay bằngawait asyncio.sleephoặcawait asyncio.sleep_ms.Với
csi.CSI.snapshot: dùng async snapshot wrapper mà trang capture xây dựng.Với tính toán dài (xử lý ảnh, mã hoá JPEG): chấp nhận chi phí hoặc chia công việc thành các đoạn mà
awaitgiữa các lần lặp.
Asyncio không thể làm cho một lời gọi đồng bộ trở thành không chặn. Nó chỉ có thể cho phép các coroutine khác chạy trong khi thứ khác đang await.