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:
- ▸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
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ình | Hợp với | Ghi chú |
|---|---|---|
| async/await | RẤ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 |
| threading | I/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-threading | CPU-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
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).
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
async/await thực hiện đồng thời theo cách nào?
- 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 khiawaitlần lượt vsasyncio.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
Coroutine đầu tiên
Viết
async def chao(ten): await asyncio.sleep(0.1); return f"Hi {ten}". Chạy bằngasyncio.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
TaskGroup
Dùng
async with asyncio.TaskGroup() as tg:tạo 3 task. Quan sát: nếu một taskraise, 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 →
TaskGrouphuỷ phần còn lại & gom lỗi (ExceptionGroup). - 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.sleepchặn cả luồng (không nhường loop) → dùngawait asyncio.sleep(2)để nhường quyền khi chờ. - 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).