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.4. Thay đổi trạng thái chia sẻ qua các await

Lập lịch hợp tác đảm bảo rằng một coroutine có CPU của riêng mình giữa các await, nhưng ngay khi nó await, một coroutine khác có thể chạy. Nếu hai coroutine sửa đổi cùng một cấu trúc dữ liệu theo các bước bao gồm await, các thao tác của chúng có thể xen kẽ nhau theo những cách làm hỏng cấu trúc:

# 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))

Đối với trạng thái chỉ được thay đổi giữa các await bên trong một coroutine, không cần đồng bộ hoá. Đối với trạng thái được thay đổi qua các await và được truy cập từ nhiều hơn một coroutine, hãy bọc phần critical section trong một Lock.

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ằng await asyncio.sleep hoặc await 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à await giữ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.