Bài 19. Lập trình bất đồng bộ với asyncio

Khi chương trình phải chờ mạng, tệp hay cơ sở dữ liệu, bất đồng bộ giúp làm nhiều việc cùng lúc trên một luồng. Bài này giải thích async/await, asyncio và so sánh với đa luồng, đa tiến trình.

50 phútNâng cao
Bạn sẽ học được
  • Phân biệt đồng thời và song song
  • Viết coroutine với async/await
  • Chạy nhiều tác vụ với gather, TaskGroup, timeout
  • Chọn đúng: asyncio, threading hay multiprocessing
Thuật ngữ cần biết
Coroutine
Hàm async def có thể tạm dừng tại await.
Event loop
Vòng lặp điều phối các coroutine.
I/O-bound
Việc chủ yếu chờ vào/ra (mạng, đĩa).
CPU-bound
Việc chủ yếu tính toán.

Khái niệm và lý thuyết

Đồng thời (concurrency): xen kẽ nhiều việc, ví dụ một đầu bếp vừa chờ nước sôi vừa thái rau. Song song (parallelism): nhiều việc chạy thật sự cùng lúc trên nhiều lõi CPU. asyncio đạt đồng thời trên một luồng: khi một coroutine await chờ mạng, event loop chuyển sang coroutine khác.

Loại việcCông cụ phù hợp
Nhiều yêu cầu mạng, I/Oasyncio (httpx, aiofiles)
I/O với thư viện không hỗ trợ asyncthreading, asyncio.to_thread
Tính toán nặngmultiprocessing, concurrent.futures.ProcessPoolExecutor

Từ Python 3.13–3.14 có bản Python free-threaded (không GIL) được hỗ trợ chính thức, giúp đa luồng chạy song song thật cho tác vụ tính toán; đây là hướng phát triển lớn của Python.

async và await

import asyncio, time

async def nau(mon: str, giay: float) -> str:
    print(f"Bắt đầu {mon}")
    await asyncio.sleep(giay)            # chờ không chặn
    return f"{mon} xong"

async def main():
    bd = time.perf_counter()
    kq = await asyncio.gather(nau("Cơm", 3), nau("Canh", 2), nau("Rau", 1))
    print(kq, f"{time.perf_counter() - bd:.1f}s")   # khoảng 3s thay vì 6s

asyncio.run(main())

TaskGroup và timeout

async def main():
    try:
        async with asyncio.timeout(5):
            async with asyncio.TaskGroup() as tg:
                t1 = tg.create_task(nau("Phở", 2))
                t2 = tg.create_task(nau("Bún", 1))
        print(t1.result(), t2.result())
    except TimeoutError:
        print("Quá thời gian")

TaskGroup đảm bảo: nếu một tác vụ lỗi, các tác vụ còn lại được hủy và lỗi được báo đầy đủ, an toàn hơn gather.

Giới hạn đồng thời

sem = asyncio.Semaphore(5)               # tối đa 5 yêu cầu cùng lúc
async def tai(client, url):
    async with sem:
        r = await client.get(url)
        return r.status_code

Kết hợp mã đồng bộ

import requests
async def main():
    # chạy hàm chặn trong luồng phụ, không làm treo event loop
    r = await asyncio.to_thread(requests.get, "https://example.com", timeout=10)
    print(r.status_code)
Mẹo hay

Trong Jupyter đã có sẵn event loop: dùng trực tiếp await main() thay cho asyncio.run(main()).

Lưu ý

Gọi hàm chặn như time.sleep hay requests.get trực tiếp trong coroutine sẽ làm đứng mọi tác vụ khác. Dùng phiên bản async hoặc to_thread.

Thực hành
  1. Mô phỏng tải 10 tệp với thời gian ngẫu nhiên, đo tổng thời gian tuần tự và bất đồng bộ.
  2. Dùng httpx.AsyncClient kiểm tra trạng thái 30 website, giới hạn 5 kết nối cùng lúc.
  3. Thêm timeout 3 giây cho từng yêu cầu.
  4. Thử tính tổng số nguyên tố đến 5 triệu bằng ProcessPoolExecutor và so sánh.
Tóm tắt bài học
  • asyncio: đồng thời trên một luồng, hợp với I/O.
  • async def tạo coroutine; await để chờ.
  • gather, TaskGroup, timeout, Semaphore điều phối tác vụ.
  • Tính toán nặng dùng đa tiến trình hoặc Python free-threaded.