Câu 1Cho async def hello(): return 'Hi', hello() trả về cái gì?
Cơ bản async / await — Tăng tốc chương trình bằng cách tận dụng thời gian chờ
Học cơ chế coroutine, event loop và asyncio.sleep, rồi cách dùng async def / await để chuyển thời gian chờ I/O sang task khác và tăng tốc xử lý.
async / await là cơ chế để làm việc khác trong khi chờ I/O (Input/Output — đọc file, gọi mạng, truy vấn DB và các thao tác mà thời gian chờ chiếm phần lớn). Nó có thể tăng tốc các tác vụ như gọi Web API 100 lần, mà không cần thoát khỏi một thread duy nhất. Bài viết này đi qua ba ý chính: coroutine, event loop, và asyncio.sleep.
Về việc chạy code ở đây
async / await xoay quanh thời gian, nhưng runner của trang này gom output print lại và hiển thị một lần sau khi script chạy xong. Cảm giác output theo thời gian thực và thời gian trôi sẽ không giống môi trường Python thật. Bài viết dùng sơ đồ để làm rõ hành vi bên trong, nhưng nếu bạn muốn xem print xuất ra theo dòng chảy hoặc cảm nhận thời gian thực, hãy chạy trong môi trường Python local với asyncio.run.
Process và Thread — bối cảnh cho async / await
Trước khi đi sâu vào async / await, hãy xác định rõ process và thread.
Một process (một chương trình đang chạy theo cách OS nhìn) là một đơn vị thực thi độc lập với không gian bộ nhớ riêng. Chạy một script Python thì bạn được một process; chạy thêm một cái khác trong terminal khác thì bạn có hai process.
Một thread (luồng thực sự chạy code bên trong process) là đơn vị dùng CPU để tiến hành code. Một process có thể có nhiều thread, nhưng một chương trình Python thông thường chạy với 1 process + 1 thread, và một lệnh chờ như time.sleep sẽ dừng thread đó.
- Đơn vị thực thi độc lập theo cách OS nhìn
- Có không gian bộ nhớ riêng
- Đơn vị thực sự chạy code bên trong process
- Python thông thường chỉ có một đang chạy
- Đổi lượt bên trong một thread
- Bạn có thể tạo bao nhiêu tùy thích với
async / await
threading (thêm thread) và multiprocessing (thêm process).async / await chỉ xử lý các coroutine ở trong cùng. Nó chuyển giữa các coroutine trên cùng một thread, không bao giờ thêm thread hay CPU — nó chỉ trao quyền điều khiển cho coroutine khác trong khi CPU rảnh chờ I/O. Để chạy thread thực sự song song, dùng threading; để thêm process, dùng multiprocessing (được trình bày trong hai bài viết tiếp theo).
Coroutine là gì — ngôi sao của async / await
Một coroutine (hàm / đối tượng được định nghĩa với async def, có thể tạm dừng tại các điểm await bên trong) là trung tâm của async / await. Một hàm thông thường chạy thẳng một mạch tới cuối khi được gọi, nhưng một coroutine có thể tạm dừng tại mỗi await bên trong và chạy tiếp khi event loop ra hiệu. Đó là nền tảng mà async được xây dựng trên đó.
Gọi một hàm async def không chạy phần thân của nó — nó chỉ trả về một đối tượng coroutine (ví dụ: coro = hello()). Phần thân chỉ bắt đầu chạy khi bạn await coro hoặc truyền nó cho asyncio.run(coro).
import asyncio
async def hello():
return "Hi"
# Gọi không chạy phần thân
coro = hello()
print(coro) # <coroutine object hello at 0x...>
# ↑ 0x... là địa chỉ bộ nhớ.
# Chỉ nghĩa là "một coroutine đã được tạo"
# await thì mới thực sự chạy nó
print(await coro) # Hi
return kết thúc nó. Các điểm await bên trong tạm dừng và tiếp tục ở giữa.Khi bạn viết await some_io(), coroutine tạm dừng và chuyển sang tác vụ khác, và các coroutine khác có thể chạy trong khi nó chờ — đó là đặc điểm cốt lõi của async.
Event loop là gì — bộ lập lịch chuyển đổi giữa các coroutine
Event loop (bộ lập lịch chuyển đổi giữa các coroutine theo lượt) là thứ mà asyncio chạy ở phía sau. Nó lặp vô tận: lấy một coroutine từ hàng đợi → chạy nó → chuyển sang coroutine khác tại await → đặt các lệnh chờ đã hoàn thành trở lại hàng đợi.
import asyncio
async def main():
print("bắt đầu")
await asyncio.sleep(0) # sleep(0) không thực sự chờ,
# nó chỉ đánh dấu "OK chuyển ở đây"
print("kết thúc")
# asyncio.run khởi động loop → chạy main → đóng loop khi xong
asyncio.run(main())
# Output:
# bắt đầu
# kết thúc
Tất cả những điều này diễn ra trên một thread duy nhất — không có lõi CPU bổ sung, chỉ là lấp thời gian chờ rảnh bằng một coroutine khác. Trong môi trường Python chạy trên trình duyệt của trang này, event loop đã đang chạy, nên bạn có thể viết await trực tiếp ở top-level mà không cần gọi asyncio.run(...).
Vì sao async / await — chuyển sang tác vụ khác trong lúc chờ
Trong code đồng bộ (sync) (kiểu viết thường, từng dòng một), time.sleep(1) chặn chương trình — CPU rảnh, nhưng chương trình bị đóng băng. Điều này cũng đúng với phản hồi Web API, truy vấn DB và hoàn thành file I/O.
async / await cho phép bạn chuyển sang tác vụ khác ở bất cứ chỗ nào bạn viết "chờ", ở lại trên cùng thread nhưng chuyển ngay khi một lệnh chờ bắt đầu.
# requests = HTTP client đồng bộ (mỗi lần một lệnh gọi)
# httpx = HTTP client hỗ trợ async (await các lệnh gọi song song)
import requests, asyncio, httpx
# Sync: gọi 3 API lần lượt → tổng 3 giây
def fetch_users_sync():
r1 = requests.get("https://api.example.com/users/1") # ← chờ 1 giây
r2 = requests.get("https://api.example.com/users/2") # ← thêm 1 giây
r3 = requests.get("https://api.example.com/users/3") # ← thêm 1 giây
return [r1.json(), r2.json(), r3.json()]
# Async: gọi cả 3 song song → xong trong ~1 giây (cái chậm nhất)
async def fetch_users_async():
async with httpx.AsyncClient() as client:
r1, r2, r3 = await asyncio.gather(
client.get("https://api.example.com/users/1"),
client.get("https://api.example.com/users/2"),
client.get("https://api.example.com/users/3"),
)
return [r1.json(), r2.json(), r3.json()]
Chi tiết asyncio.gather sẽ có ở bài tiếp theo
asyncio.gather(...) được dùng ở đây chạy nhiều coroutine đồng thời và chờ tất cả kết quả. Ngữ nghĩa chi tiết — giá trị trả về, xử lý ngoại lệ — được trình bày trong bài tiếp theo: asyncio nâng cao.
Concurrent, không phải parallel
async / await không bao giờ thêm CPU — code dùng tối đa CPU (tính toán nặng) sẽ không nhanh hơn. Nó chỉ lấp thời gian CPU rảnh trong lúc "chờ I/O / chờ mạng / sleep" bằng cách chuyển sang tác vụ khác. Đây là thực thi concurrent, không phải parallel thực sự. Để thực sự chạy trên nhiều lõi, bạn cần threading hoặc multiprocessing — được trình bày trong hai bài viết tiếp theo.
async def và await — những điều cơ bản
Một hàm được định nghĩa với async def được gọi là hàm coroutine — gọi nó không chạy phần thân, mà chỉ trả về một đối tượng coroutine. Để thực sự chạy, hãy await nó hoặc truyền cho asyncio.run().
await x nghĩa là "chờ x hoàn thành, chuyển sang tác vụ khác trong lúc đó". x có thể là một trong ba thứ: một coroutine (cái bài viết này tập trung vào), một Task, hoặc một Future (đối tượng Task được trình bày ở bài tiếp theo, cộng với đối tượng thông báo hoàn thành mà nó dùng bên trong) — trong code hằng ngày, bạn sẽ chủ yếu dùng coroutine hoặc Task.
import asyncio
# Định nghĩa hàm coroutine với async def
async def hello():
return "Hi"
# Gọi chỉ trả về đối tượng coroutine (phần thân không chạy)
print(hello()) # <coroutine object hello at 0x...>
# await chạy nó (top-level await hoạt động trong môi trường trình duyệt này)
result = await hello()
print(result) # Hi
# Trong một script Python thật, bọc với asyncio.run()
# print(asyncio.run(hello())) # Hi
| Phần tử | Ý nghĩa | Ghi chú |
|---|---|---|
| async def f(): | Định nghĩa một hàm coroutine | Gọi nó không chạy phần thân |
| f() | Tạo một đối tượng coroutine | Cần await để thực sự chạy |
| await f() | Chờ hoàn thành, chuyển sang khác | Chỉ hợp lệ bên trong hàm async |
| asyncio.sleep(N) | Chờ N giây (nhường lượt khi chờ) | Không chặn như time.sleep |
| asyncio.run(f()) | Chạy từ top level | Điểm vào chuẩn trong Python thật |
Không có await, coroutine không bao giờ chạy
Nếu bạn viết hello() đơn thuần, phần thân không bao giờ chạy, và bạn sẽ thấy cảnh báo như <coroutine object hello at 0x...> trong console. Luôn luôn await hello() hoặc chạy nó qua asyncio.run(hello()). "Gọi" và "chạy" là hai thứ khác nhau trong async — hãy phân biệt rõ.
Thực thi đồng thời chia sẻ thời gian chờ — phần thưởng của asyncio
asyncio.sleep(seconds) là phiên bản async của sleep — nó chuyển sang tác vụ khác trong lúc chờ. Kết hợp với asyncio.gather (bài tiếp theo) để chạy nhiều coroutine cùng lúc, tất cả các sleep tiến triển đồng thời, nên tổng thời gian trở thành lệnh sleep dài nhất (không phải tổng, như time.sleep sẽ cho bạn).
Bên trong, asyncio.sleep(N) đăng ký "tiếp tục coroutine này sau N giây" với loop và chuyển ra ngay lập tức. Ngược lại, time.sleep(N) là một lệnh gọi OS chặn hoàn toàn CPU — loop cũng dừng, và không coroutine nào khác có thể chạy. Dùng time.sleep bên trong hàm async sẽ phá vỡ toàn bộ ý nghĩa của async, nên hãy cẩn thận.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Bên trong một hàm async, cái nào "chờ N giây trong khi chuyển sang tác vụ khác"?
Câu 3Khối lượng công việc nào hưởng lợi nhiều nhất từ async / await?