← Lập trình Python nâng cao

Bài 15 · Nâng cao · 26 phút· Cập nhật 11/06/2026

async/await với asyncio

Biên soạn bởi Nguyễn Anh Tuấn

async/await với asyncio trong Python: coroutine, event loop, gather - đồng thời cho hàng nghìn tác vụ I/O trên một luồng duy nhất.

Bài GIL cho thấy I/O-bound hưởng lợi từ đồng thời. async/await là một cách làm điều đó trên một luồng duy nhất: khi một tác vụ ngồi chờ I/O, event loop chạy tác vụ khác. Chỉnh widget để thấy:

3 tác vụ I/O (chờ mạng) trên MỘT luồng:
tải A
300ms
tải B
200ms
tải C
500ms
tổng
Tổng thời gian ≈ 500ms - gather chạy cả 3 CÙNG LÚC: trong lúc chờ I/O của tác vụ này, loop chạy tác vụ khác → tổng ≈ tác vụ LÂU NHẤT (500ms). Tăng tốc ≈ 2× so với await lần lượt.
★ Một luồng duy nhất: event loop CHUYỂN tác vụ tại mỗi await (lúc một tác vụ ngồi chờ I/O). Đo thật trên Python 3.14.5: lần lượt 1.01s · gather 0.50s.
  • async = đồng thời HỢP TÁC: code tự nhường tại mỗi await (lúc chờ I/O).
  • Một luồng phục vụ NHIỀU tác vụ I/O → nhẹ, không cần Lock (không bị OS chen ngang).
  • gather: tổng thời gian ≈ tác vụ LÂU NHẤT, không phải tổng - vì các lần chờ chồng lấn.

basic.py

import asyncio

async def chao(ten):           # async def -> coroutine
    await asyncio.sleep(0.1)   # tam dung, nhuong loop trong luc cho
    return f"Hi {ten}"

c = chao("An")                 # CHUA chay than ham, chi tao coroutine
print(asyncio.run(chao("An"))) # asyncio.run: tao event loop & chay

Kết quả khi chạy

Hi An
  • async def tạo coroutine; gọi nó CHƯA chạy (giống generator chưa chạy).
  • await tạm dừng tới khi awaitable xong, nhường loop trong lúc chờ; chỉ dùng trong async def.
  • asyncio.run(coro()) là điểm vào cấp ngoài cùng - tạo & đóng event loop.

await lần lượt thì vẫn nối tiếp. Để CHẠY CÙNG LÚC, gom bằng asyncio.gather:

gather.py - đo thật trên Python 3.14.5

import asyncio, time

async def tai(ten, giay):
    await asyncio.sleep(giay)          # gia lap cho I/O
    return ten

async def main():
    t = time.perf_counter()
    await tai("A", 0.3); await tai("B", 0.2); await tai("C", 0.5)   # LAN LUOT
    print(f"lan luot: {time.perf_counter()-t:.2f}s")

    t = time.perf_counter()
    await asyncio.gather(tai("A", 0.3), tai("B", 0.2), tai("C", 0.5)) # DONG THOI
    print(f"gather:   {time.perf_counter()-t:.2f}s")

asyncio.run(main())

Kết quả khi chạy

lan luot: 1.01s
gather:   0.50s
  • await lần lượt = TỔNG thời gian chờ (1.0s); gather = chờ CHỒNG LẤN ≈ MAX (0.5s).
  • gather(*coros) trả list kết quả theo thứ tự bạn truyền vào.
  • Tất cả trên MỘT luồng - không khoá, không race trên dữ liệu thường.

Event loop là một vòng lặp trên một luồng: nó chạy một coroutine tới khi gặp await (điểm “ngồi chờ”), treo nó và chạy coroutine khác; khi I/O xong, resume coroutine đó.

  • await ~ điểm yield: coroutine TẠM DỪNG & trả quyền (xây trên chính cơ chế generator - nhớ bài iterator/generator).
  • Loop chỉ chuyển tại await → bạn biết CHÍNH XÁC nơi có thể bị chen → ít bug đồng thời hơn threads.
  • Một luồng: nếu một coroutine không bao giờ await mà làm việc nặng → cả loop đứng hình.

Đừng CHẶN event loop

Trong coroutine, đừng dùng time.sleep() hay vòng lặp CPU nặng - chúng giữ luôn luồng, mọi tác vụ async khác đứng. Dùng await asyncio.sleep(), thư viện I/O async; việc CPU nặng thì đẩy sang tiến trình (loop.run_in_executor / ProcessPool).

real.py

import asyncio, httpx       # uv add httpx

async def tai(client, url):
    r = await client.get(url, timeout=10)   # I/O async: nhuong loop khi cho
    return r.status_code

async def main(urls):
    async with httpx.AsyncClient() as client:     # async with: dong client dung cach
        async with asyncio.TaskGroup() as tg:     # TaskGroup (3.11+): quan ly nhom task
            tasks = [tg.create_task(tai(client, u)) for u in urls]
    return [t.result() for t in tasks]

# asyncio.run(main([...]))
  • httpx.AsyncClient + await client.get: gọi nhiều API đồng thời (requests không async).
  • async with: context manager BẤT đồng bộ (mở/đóng tài nguyên async); async for: lặp nguồn async.
  • asyncio.TaskGroup (3.11+) là cách mới khuyến nghị: một task lỗi → huỷ gọn phần còn lại.
Mô hìnhHợp vớiGhi chú
async/awaitRẤT NHIỀU I/O đồng thời (web server, scraper, chat)1 luồng, nhẹ, không Lock; cần thư viện async
threadingI/O vừa phải; thư viện chỉ-đồng-bộGIL nhả khi chờ; cần Lock cho dữ liệu chung
multiprocessing / free-threadingCPU-bound (tính toán)song song thật trên nhiều lõi

Hết phần đồng thời - sẵn sàng capstone

Bạn đã đủ bộ công cụ đồng thời: GIL (vì sao), threading/multiprocessing, free-threading, và async/await. Bài cuối: capstone - 3 dự án ghép tất cả (thu thập web đồng thời, CLI + SQLite có test, dịch vụ async).

Câu hỏi thường gặp

async là đồng thời HỢP TÁC trên MỘT luồng: code tự nhường quyền tại mỗi await (khi ngồi chờ I/O), event loop chạy tác vụ khác trong lúc đó. Khác threads (do OS chuyển bất cứ lúc nào → cần Lock): async chỉ chuyển tại await nên KHÔNG có race trên dữ liệu thường, và NHẸ hơn nhiều - phục vụ hàng nghìn kết nối I/O trên một luồng.

async def tạo một COROUTINE. Gọi ham() KHÔNG chạy thân hàm - chỉ trả về một coroutine (giống generator chưa chạy). Phải await nó (trong một coroutine khác) hoặc đưa vào asyncio.run(ham()) ở cấp ngoài cùng thì event loop mới thực thi.

await tạm dừng coroutine hiện tại CHO TỚI KHI thứ được await xong (vd một I/O), TRẢ quyền cho event loop chạy tác vụ khác trong lúc chờ, rồi tiếp tục từ chỗ đó. Chỉ await được "awaitable" (coroutine, Task, Future). await chỉ dùng được bên trong async def.

Cả hai chạy nhiều coroutine ĐỒNG THỜI. asyncio.gather(*coros) trả về list kết quả theo thứ tự. asyncio.TaskGroup (3.11+, dùng async with) là cách mới được khuyến nghị: quản lý nhóm task an toàn hơn - nếu một task lỗi, nó huỷ các task còn lại gọn gàng. Dùng TaskGroup cho code mới.

Vì chỉ có MỘT luồng, nếu một coroutine làm việc NẶNG mà không await (vd time.sleep, vòng lặp tính toán lớn, gọi hàm I/O đồng bộ), nó GIỮ luôn loop → mọi tác vụ khác đứng hình. Quy tắc: trong async, dùng await asyncio.sleep (không time.sleep), dùng thư viện async cho I/O; việc CPU nặng thì đẩy sang tiến trình (run_in_executor).

async hợp nhất khi có RẤT NHIỀU tác vụ I/O đồng thời (hàng nghìn kết nối: web server, scraper, chat) - nhẹ và không cần Lock. threads hợp khi cần đồng thời I/O VỪA PHẢI hoặc gọi thư viện chỉ-đồng-bộ. CPU-bound thì cả hai đều không giúp (xem bài GIL) - dùng tiến trình / free-threading.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

async/await thực hiện đồng thời theo cách nào?

  1. 1

    await lần lượt vs gather

    Dùng widget Bước 1; rồi tự viết: 3 coroutine asyncio.sleep(0.3/0.2/0.5), đo thời gian khi await lần lượt vs asyncio.gather.

    Hoàn thành khi: Lần lượt ≈ 1.0s (tổng); gather ≈ 0.5s (max). Bạn giải thích vì sao một luồng vẫn nhanh hơn.

  2. 2

    Coroutine đầu tiên

    Viết async def chao(ten): await asyncio.sleep(0.1); return f"Hi {ten}". Chạy bằng asyncio.run.

    Hoàn thành khi: Gọi chao("An") trả coroutine; asyncio.run(chao("An")) mới thực thi và trả "Hi An".

  3. 3

    TaskGroup

    Dùng async with asyncio.TaskGroup() as tg: tạo 3 task. Quan sát: nếu một task raise, các task kia bị huỷ thế nào.

    Hoàn thành khi: Ba task chạy đồng thời; một task lỗi → TaskGroup huỷ phần còn lại & gom lỗi (ExceptionGroup).

  4. 4

    Đừng chặn loop

    Bằng lời: vì sao đặt time.sleep(2) trong một coroutine lại làm “đứng hình” mọi tác vụ async khác? Sửa thế nào?

    Hoàn thành khi: time.sleep chặn cả luồng (không nhường loop) → dùng await asyncio.sleep(2) để nhường quyền khi chờ.

  5. 5

    Chọn mô hình

    Cho: (a) server nhận 5000 kết nối WebSocket; (b) nén 100 video. Mỗi việc nên async, threads, hay processes?

    Hoàn thành khi: (a) async (rất nhiều I/O đồng thời, nhẹ); (b) processes/free-threading (CPU-bound).