8.4. Gather

Chương trước đã chỉ ra cách asyncio.create_task() lên lịch một coroutine và không chờ nó. Thao tác bổ sung -- chạy nhiều awaitable đồng thời và chờ tất cả chúng -- là asyncio.gather().

8.4.1. Hình dạng cơ bản

import asyncio

async def fetch(name, delay_ms):
    await asyncio.sleep_ms(delay_ms)
    return name

async def main():
    results = await asyncio.gather(
        fetch("a", 100),
        fetch("b", 200),
        fetch("c", 50),
    )
    print(results)

asyncio.run(main())

Đầu ra:

['a', 'b', 'c']

Có hai điều cần chú ý. Thứ nhất, danh sách kết quả theo thứ tự của đối số truyền vào gather, không phải thứ tự mà các coroutine hoàn thành -- "c" trả về trước nhưng vẫn là mục thứ ba. Thứ hai, lần gọi mất tổng cộng 200 ms, không phải 350 ms: ba lần sleep chạy đồng thời, và gather() trả về ngay khi lần chậm nhất hoàn thành.

Cả hai sự thật đều xuất phát từ cùng một nguồn. gather() bọc từng đối số chưa phải là task (các coroutine trong ví dụ), lên lịch chúng trên vòng lặp, tạm dừng coroutine đang gọi cho đến khi tất cả chúng hoàn thành, rồi trả về kết quả của chúng theo thứ tự ban đầu.

8.4.2. Khi nào nên dùng

Bất cứ khi nào ứng dụng có N thao tác awaitable và muốn kết quả của tất cả chúng trước khi tiếp tục. Các ví dụ điển hình:

  • Phát nhiều yêu cầu mạng song song và chờ tất cả phản hồi.

  • Đọc từ nhiều cảm biến đồng thời trước khi xử lý kết quả kết hợp.

  • Kết hợp nhiều task trợ giúp tồn tại ngắn ở cuối một giai đoạn ứng dụng.

Đây không phải công cụ phù hợp cho các task nền chạy lâu dài mà ứng dụng khởi động và để chạy suốt vòng đời chương trình -- những task đó vẫn là công việc của create_task(). gather() dành cho mẫu fan-out / fan-in: chia nhỏ công việc, thực hiện đồng thời, tập hợp lại.

8.4.3. Ngoại lệ trong nhóm

Nếu bất kỳ awaitable nào trong nhóm phát ra ngoại lệ, hành vi mặc định là phát lại ngoại lệ ra khỏi lệnh gọi gather. Các coroutine anh em chưa hoàn thành sẽ bị hủy trong nền.

Đó thường là điều mà ứng dụng muốn -- một trong N công việc song song thất bại, vì vậy thao tác kết hợp đã thất bại, vì vậy không tiếp tục lãng phí thời gian vào phần còn lại. Đôi khi ứng dụng muốn ngược lại: để mỗi awaitable hoàn thành (hoặc thất bại) độc lập, và kiểm tra kết quả sau. Truyền return_exceptions=True cho trường hợp đó:

results = await asyncio.gather(
    fetch_or_fail("ok"),
    fetch_or_fail("bad"),
    return_exceptions=True,
)
# results == ["ok-value", OSError(...)]

Mỗi mục trong danh sách được trả về giờ đây là giá trị trả về bình thường hoặc ngoại lệ mà awaitable tương ứng đã phát ra. Người gọi kiểm tra isinstance(r, Exception) để phân biệt chúng.

8.4.4. Hủy

Hủy bản thân gather -- bằng cách hủy task nào đang await nó -- sẽ hủy mọi awaitable vẫn đang chạy bên trong nó. Trang timeouts và hủy đề cập đến cách hủy lan truyền qua chuỗi gọi một cách chi tiết.