Học bằng cách đọc theo thứ tự

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.

Nơi async / await phát huy hiệu quả
Gọi 100 Web APIđồng thờiChạy truy vấn DBđồng thờiXử lý file I/Osong songWeb scrapingnhiều trang cùng lúc
Tốt nhất cho công việc bị chi phối bởi thời gian chờ. async chuyển sang tác vụ khác khi CPU rảnh (chờ I/O / mạng / sleep), giữ chương trình trên một thread trong khi vẫn làm cho mọi thứ nhanh hơn.

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õ processthread.

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 đó.

Process / Thread / Coroutine — các lớp lồng nhau
Process (một ứng dụng Python)
  • Đơn vị thực thi độc lập theo cách OS nhìn
  • không gian bộ nhớ riêng
Thread (luồng thực thi)
  • Đơn vị thực sự chạy code bên trong process
  • Python thông thường chỉ có một đang chạy
Coroutine (một hàm async def)
  • Đổ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
Ba mức lồng: một process chứa một thread, mà bên trong chứa các coroutine. async / await chỉ lập lịch cho các coroutine ở trong cùng — nó không thêm thread hay CPU. Đó là điểm khác biệt chính so với 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
Trạng thái coroutine
async def f()định nghĩaf() được gọicoroutine tạo raawait f()→ bắt đầu chạyreturn→ xong
Gọi một hàm async def không chạy phần thân — nó chỉ trả về một đối tượng coroutine. await thực sự bắt đầu thực thi, và 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
Event loop hoạt động như thế nào
Lấy từhàng đợiChạycoroutineTại await,chuyển raI/O xong →trở lại hàng đợi
Event loop lấy một coroutine có thể chạy từ hàng đợi và chạy nó, chuyển sang coroutine khác khi gặp await, và đặt một coroutine trở lại hàng đợi khi I/O của nó hoàn thành — lặp mãi trên một thread duy nhất.

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.

async — nhiều tác vụ đổi lượt
A đang chạyB đang chờC đang chờA: await→ chuyển raB đang chạyC đang chờA đang chờB: await→ chuyển raC đang chạyswitchswitch
Ba coroutine chia nhau một CPU trên cùng một thread. Mỗi tác vụ chuyển ra tại await khi bắt đầu chờ, và tác vụ tiếp theo chạy. Thời gian chờ tiến triển song song mà không bao giờ rời khỏi thread duy nhất — đó là cốt lõi của async.
Concurrent vs Parallel
ConcurrentChuyển giữa cáctác vụ trên một CPUasync / await(bài này)ParallelNhiều lõi CPUchạy cùng lúcthreading /multiprocessing(bài tiếp theo)
Concurrent = chuyển giữa các tác vụ trên một CPU (cái mà async / await đem lại). Parallel = nhiều lõi CPU thực sự chạy cùng lúc (đạt được với threading / multiprocessing). async sẽ không tăng tốc code dùng 100% CPU.

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 coroutinegọ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
async def và await
async def hello(): return 'Hi'kết quả hello()= một coroutine(thân chưa chạy)await hello()→ trả về 'Hi'chỉ gọiawait
Định nghĩa với async defgọi đơn thuần trả về một đối tượng coroutine với phần thân chưa chạy → await thực sự chạy nó và trả ra kết quả. Đó là quy tắc tối thiểu của async / await.
Phần tửÝ nghĩaGhi chú
async def f():Định nghĩa một hàm coroutineGọi nó không chạy phần thân
f()Tạo một đối tượng coroutineCần await để thực sự chạy
await f()Chờ hoàn thành, chuyển sang khácChỉ 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õ.

Viết một hàm async nhỏ mô phỏng một lệnh gọi API, rồi chạy nó với await. Bạn sẽ dùng asyncio.sleep(0.5) để giả lập thời gian phản hồi 0.5 giây.

① Hãy thêm import asyncio.

② Hãy định nghĩa async def fetch_user(user_id): — in f"user {user_id} bắt đầu", await asyncio.sleep(0.5) để chờ 0.5 giây, in f"user {user_id} xong", rồi return f"User{user_id}".

③ Hãy chạy với result = await fetch_user(1) và lưu giá trị (top-level await hoạt động ở đây).

④ Hãy in f"result: {result}".

(Chạy thành công và phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

Thực thi đồng thời chia sẻ thời gian chờ — phần thưởng của asyncio

asyncio.sleep(seconds)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)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.

Bên trong asyncio.sleep
awaitasyncio.sleep(1)Báo loop:"đánh thức sau 1s"Trong lúc đó cáccoroutine khác chạy1 giây sau→ tiếp tục
asyncio.sleep(N) đăng ký "tiếp tục sau N giây" với event loopchuyển ra. Trong khoảng thời gian đó, các coroutine khác chạy. Sau N giây coroutine trở lại hàng đợi và tiếp tục.
time.sleep vs asyncio.sleep
time.sleep(1)(sync)Chặn CPUtrong 1 giâyCác tác vụ asynckhác cũng đóng băngasyncio.sleep(1)(async)Nhường looptrong 1 giâyTác vụ kháccó thể tiến
time.sleep chặn hoàn toàn CPU, nên các tác vụ async khác không thể chạy. asyncio.sleep chuyển sang tác vụ khác, cho phép chúng tiến triển — luôn dùng asyncio.sleep bên trong hàm async.

Gọi cùng fetch_user 3 lần song song và quan sát công việc tuần tự 1.5 giây xong trong 0.5 giây. Bạn sẽ dùng asyncio.gather để thực thi song song (chi tiết đầy đủ ở bài tiếp theo).

① Hãy thêm import asyncio.

② Hãy định nghĩa async def fetch_user(user_id): — giống như bài tập trước (in bắt đầu → await asyncio.sleep(0.5) → in xong → return).

③ Hãy chạy với results = await asyncio.gather(fetch_user(1), fetch_user(2), fetch_user(3)) để khởi 3 lệnh gọi song song.

④ Hãy in danh sách kết quả dạng f"results: {results}".

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Cho async def hello(): return 'Hi', hello() trả về cái gì?

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?