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.
- 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
- 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ệc | Công cụ phù hợp |
|---|---|
| Nhiều yêu cầu mạng, I/O | asyncio (httpx, aiofiles) |
| I/O với thư viện không hỗ trợ async | threading, asyncio.to_thread |
| Tính toán nặng | multiprocessing, 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_codeKế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)Trong Jupyter đã có sẵn event loop: dùng trực tiếp await main() thay cho asyncio.run(main()).
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.
- 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ộ.
- 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.
- Thêm timeout 3 giây cho từng yêu cầu.
- Thử tính tổng số nguyên tố đến 5 triệu bằng ProcessPoolExecutor và so sánh.
- 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.