Hướng dẫn sử dụng AnyIO trong Python: Làm chủ Structured Concurrency thay thế asyncio thuần

Python tutorial - IT technology blog
Python tutorial - IT technology blog

So sánh 3 trường phái xử lý bất đồng bộ trong Python

Viết async trong Python ban đầu rất đơn giản với vài cú pháp async/await cơ bản. Nhưng khi hệ thống mở rộng, ví dụ worker cần xử lý 500 request/giây, sự khác biệt giữa các mô hình concurrency bắt đầu bộc lộ rõ:

  • 1. asyncio truyền thống (Unstructured Concurrency): Khởi tạo task qua asyncio.create_task() hoặc asyncio.gather(). Task chạy kiểu “bắn rồi quên” (fire-and-forget). Nó sống độc lập và tách rời hoàn toàn khỏi vòng đời của hàm gọi.
  • 2. Trio (Pure Structured Concurrency): Đưa triết lý luồng có cấu trúc vào Python. Mọi task con bắt buộc sinh ra và kết thúc trong một scope cụ thể (Nursery). Nhờ đó, Trio triệt tiêu hoàn toàn lỗi task mồ côi (orphan tasks).
  • 3. AnyIO (Structured Concurrency đa nền tảng): Mang chuẩn Structured Concurrency từ Trio sang thế giới asyncio. Bạn vừa có TaskGroup an toàn, vừa tận dụng được hệ sinh thái thư viện khổng lồ của asyncio mà không bị bó buộc vào một runtime duy nhất.

Ưu và nhược điểm từng giải pháp từ góc nhìn thực chiến

Một pipeline automation mình từng vận hành phình từ 200 dòng lên hơn 3.000 dòng. Đó cũng là lúc các lỗi race condition và rò rỉ RAM âm thầm xuất hiện.

1. asyncio thuần: Tiện lợi nhưng nhiều bẫy ngầm

asyncio có sẵn trong thư viện chuẩn, tài liệu phong phú. Dù vậy, cơ chế quản lý vòng đời lỏng lẻo tiềm ẩn nhiều rủi ro:

  • Mất task do Garbage Collector: Nếu gọi create_task() mà không lưu biến reference, bộ thu gom rác (GC) có thể dọn sạch task ngay khi nó đang chạy. Lỗi kinh điển này đã được Python docs chính thức cảnh báo từ bản 3.10.
  • Task mồ côi ngốn tài nguyên: Trong một nhóm 10 task, nếu task số 2 lỗi, 9 task còn lại vẫn tiếp tục chạy ngầm. Hàng chục connection database hoặc socket có thể bị treo vô thời hạn.
  • Nuốt lỗi hủy task: Khối except Exception: vô tình nuốt mất asyncio.CancelledError, khiến quy trình shutdown của app bị đơ hoàn toàn.

2. Trio: Kiến trúc chuẩn mực nhưng kén hệ sinh thái

Trio thiết kế nursery cực kỳ chặt chẽ, gom lỗi rõ ràng. Điểm trừ lớn nhất là tính tương thích. Bạn gần như không thể nhúng Trio vào các service đã dựng sẵn với Uvicorn, Celery hay các thư viện bám chặt vào event loop của asyncio.

3. AnyIO: Lớp trừu tượng thực dụng

AnyIO đóng vai trò như một abstraction layer. Nó chạy mượt trên cả backend asyncio lẫn trio. Các framework hàng đầu như FastAPI (qua Starlette) hay HTTPX đều dùng AnyIO làm nền tảng xử lý async bên dưới.

  • Ưu điểm: Cung cấp TaskGroup chặt chẽ, kiểm soát cancel scope linh hoạt, đồng bộ dữ liệu qua Streams và tương thích 100% với event loop mặc định.
  • Nhược điểm: Phải cài thêm dependency ngoài (pip install anyio). Lập trình viên cũng cần bỏ thói quen bắn task tùy tiện để chuyển sang tư duy quản lý theo scope.

Khi nào nên chọn giải pháp nào?

  • Giữ asyncio thuần: Phù hợp với script cào dữ liệu đơn giản, crawl 20-30 trang web tĩnh với vài coroutine độc lập không phụ thuộc lẫn nhau.
  • Dùng AnyIO ngay: Cần thiết cho worker xử lý hàng nghìn background job, hệ thống microservice chịu tải cao, hoặc bất kỳ ứng dụng nào cần dọn sạch tài nguyên khi client disconnect.

Hướng dẫn triển khai AnyIO thực tế

Bước 1: Cài đặt thư viện

pip install anyio

Bước 2: Khởi chạy với anyio.run

Thay vì asyncio.run(), bạn khởi động qua anyio.run(). AnyIO mặc định chọn backend asyncio:

import anyio

async def main():
    print("Hệ thống bắt đầu chạy...")
    await anyio.sleep(1)
    print("Hoàn tất tác vụ!")

if __name__ == "__main__":
    anyio.run(main)

Bước 3: Gom nhóm tác vụ an toàn với TaskGroup

Quy tắc cốt lõi trong AnyIO là luôn gom các task liên quan vào create_task_group(). Khối async with đảm bảo code chỉ thoát ra khi mọi task con đã xong. Nếu một task crash, toàn bộ task còn lại trong nhóm sẽ lập tức bị hủy:

import anyio

async def fetch_service_data(service_id: int):
    print(f"[Service {service_id}] Đang tải dữ liệu...")
    await anyio.sleep(service_id)
    if service_id == 2:
        raise ValueError("Dịch vụ 2 mất kết nối bất ngờ!")
    print(f"[Service {service_id}] Hoàn tất.")

async def main():
    try:
        async with anyio.create_task_group() as tg:
            for i in [1, 2, 3]:
                tg.start_soon(fetch_service_data, i)
    except* ValueError as eg:
        # Trên Python 3.11+, AnyIO sử dụng ExceptionGroup gốc
        print(f"Bắt lỗi tập trung từ TaskGroup: {eg.exceptions}")

anyio.run(main)

Lợi ích thấy rõ: Khi service 2 ném lỗi ValueError, AnyIO lập tức gửi tín hiệu cancel tới service 3 đang chạy dở. Bạn không còn phải lo task 3 chạy chơ vơ ngoài tầm kiểm soát.

Bước 4: Kiểm soát timeout sạch sẽ với move_on_after

Thay vì bọc asyncio.wait_for() trong khối try/except rườm rà, AnyIO cung cấp context manager move_on_after() tiện dụng hơn nhiều:

import anyio

async def long_running_task():
    # Giới hạn toàn bộ khối code này trong tối đa 2.5 giây
    with anyio.move_on_after(2.5) as scope:
        print("Bắt đầu đồng bộ dữ liệu nặng...")
        await anyio.sleep(5)
        print("Dòng này sẽ không bao giờ được in ra nếu timeout.")

    if scope.cancelled_caught:
        print("Đã quá thời gian quy định! Tự động dừng tác vụ an toàn.")

anyio.run(long_running_task)

Khác với fail_after() sẽ ném TimeoutError, move_on_after() âm thầm thoát khỏi block khi chạm ngưỡng thời gian. Bạn chỉ cần kiểm tra cờ scope.cancelled_caught để quyết định logic tiếp theo.

Bước 5: Truyền dữ liệu an toàn với Memory Object Streams

Với mô hình Producer – Consumer, hãy ưu tiên luồng Stream của AnyIO thay vì asyncio.Queue. Điểm mạnh ở đây là stream hỗ trợ đóng luồng hai chiều rất tường minh:

import anyio

async def producer(send_stream):
    async with send_stream:
        for item in ["log_1.json", "log_2.json", "log_3.json"]:
            print(f"Đẩy vào hàng đợi: {item}")
            await send_stream.send(item)
            await anyio.sleep(0.5)

async def consumer(receive_stream):
    async with receive_stream:
        async for message in receive_stream:
            print(f"Xử lý thành công: {message}")

async def main():
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=10)
    async with anyio.create_task_group() as tg:
        tg.start_soon(producer, send_stream)
        tg.start_soon(consumer, receive_stream)

anyio.run(main)

Kinh nghiệm thực chiến khi ứng dụng AnyIO

  • Tránh bắn task rời rạc: Truyền trực tiếp task_group vào service layer khi cần tạo sub-task. Thói quen này giữ cây tác vụ luôn minh bạch.
  • Đưa hàm blocking sang worker thread: Khi phải đọc file 500MB hoặc gọi thư viện đồng bộ, hãy dùng await anyio.to_thread.run_sync(blocking_func, arg) thay vì tự cấu hình ThreadPoolExecutor.
  • Bắt lỗi chính xác với ExceptionGroup: Trên Python 3.11+, hãy dùng cú pháp except* để bóc tách từng ngoại lệ riêng biệt bắn ra từ các task chạy song song.
Share: