Chương 08 · Async Python

Async Python: asyncio, async/await

Python được sinh ra trong thời đại blocking I/O là chuẩn mực. Nhưng web hiện đại đòi hỏi một server xử lý hàng ngàn kết nối đồng thời. Module asyncio (Python 3.4+, hoàn thiện ở 3.7+ với async/await) mang đến giải pháp: concurrency với một thread duy nhất, rất giống mô hình event loop của JavaScript — nhưng phải hiểu rõ GIL, coroutine, và sự khác biệt với threading/multiprocessing.

Độ dài: ~1100 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-7
🎯 Mục tiêu chương
  • Phân biệt rõ ràng concurrencyparallelism.
  • Hiểu GIL (Global Interpreter Lock) và ảnh hưởng đến threading.
  • So sánh 4 mô hình: sync / threading / multiprocessing / asyncio.
  • Định nghĩa coroutine với async def và schedule chúng vào event loop.
  • Master await, asyncio.run, gather, create_task, wait_for.
  • Dùng asyncio.Queue cho pattern producer–consumer.
  • Sử dụng async with, async for đúng ngữ cảnh.
  • Tránh bẫy "block event loop" khi trộn sync vào async; dùng run_in_executor.
  • Biết hệ sinh thái async: aiohttp, httpx, aiofiles, asyncpg, motor.

1. Concurrency vs Parallelism

Hai khái niệm này thường bị nhầm lẫn vì cùng gợi ý "làm nhiều thứ một lúc". Nhưng ở mức kỹ thuật, chúng không giống nhau:

🧠 Mental model — đầu bếp và bếp

Một đầu bếp đứng nấu 3 món cùng lúc: nồi súp đang sôi (để đó), món xào đang đảo, lò nướng đang nướng. Anh ta không nướng và xào đồng thời — anh ta chuyển qua chuyển lại giữa các việc. Đây là concurrency: nhiều task xen kẽ trên một resource duy nhất.

Một nhà hàng có 3 đầu bếp, mỗi người một món. Cả 3 nấu thật sự cùng một lúc — đây mới là parallelism: cần nhiều resource (nhiều CPU core).

Khái niệm Định nghĩa Yêu cầu phần cứng Ví dụ Python
Concurrency Nhiều task được interleave trên cùng một resource. Cảm giác "chạy cùng lúc" nhưng thực ra chuyển qua lại. 1 CPU core là đủ. asyncio, threading (CPython).
Parallelism Nhiều task chạy thật sự cùng lúc trên các CPU core khác nhau. Cần nhiều CPU core / nhiều máy. multiprocessing, concurrent.futures.ProcessPoolExecutor.

Một chương trình có thể là (concurrent + không parallel) — ví dụ asyncio chạy 1000 coroutine trên 1 thread. Có thể là (parallel + không concurrent) — ví dụ 4 tiến trình hoàn toàn độc lập, không phối hợp gì. Có thể vừa concurrent vừa parallel — web server thực tế: nhiều worker process, mỗi process chạy event loop với nhiều coroutine.

📘 Câu nói nổi tiếng của Rob Pike

"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." Concurrency là tổ chức code; parallelism là thực thi vật lý.

2. GIL — Global Interpreter Lock

GIL là một mutex toàn cục trong CPython (bản tham chiếu của Python). Quy tắc: tại bất kỳ thời điểm nào, chỉ duy nhất một thread được phép thực thi Python bytecode. Các thread khác phải chờ.

Hệ quả nghe có vẻ "tiêu cực" nhưng cần hiểu chính xác:

  • Threading không giúp tăng tốc CPU-bound trong Python thuần. Hai thread cùng tính toán pi — vẫn chỉ một CPU làm việc.
  • Nhưng threading vẫn lợi cho I/O-bound: khi một thread block I/O (đọc socket, đọc file), nó giải phóng GIL cho thread khác chạy.
  • Các thư viện C (NumPy, SciPy, Pillow) thường release GIL trong vòng tính toán C, nên threading có thể song song hoá những phần đó.
  • Để parallel CPU-bound trong Python thuần, dùng multiprocessing — mỗi process có GIL riêng.
  • Python 3.13 đã có chế độ thử nghiệm "no-GIL" (PEP 703 / free-threaded build), nhưng chưa phổ biến trong production năm 2026.
Threading + GIL — CPU-bound thuần Python
Thread A
RUN WAIT GIL RUN WAIT GIL
Thread B
WAIT GIL RUN WAIT GIL RUN
RUN đang thực thi Python bytecode (giữ GIL) WAIT GIL đang chờ GIL

Tổng thời gian ≈ tổng thời gian chạy 1 thread tuần tự. Tốc độ thậm chí chậm hơn vì context-switch.

Threading + GIL — I/O-bound (network/file)
Thread A
RUN I/O wait (release GIL) RUN
Thread B
WAIT RUN I/O wait
I/O đang chờ I/O — Python release GIL, thread khác tận dụng.

Hai thread overlap được trong khoảng I/O. Tổng thời gian giảm đáng kể. Đây là lý do threading vẫn hữu ích cho I/O-bound dù có GIL.

3. So sánh 4 mô hình

Bảng so sánh tổng quan giúp chọn đúng công cụ cho đúng workload:

Mô hình Concurrency Parallelism Ảnh hưởng GIL Shared state Overhead Use case
Sync Không Không Trực tiếp Rất thấp Script đơn giản, CLI tool, batch nhỏ.
Threading Không cho CPU-bound (Python thuần) Bị giới hạn Trực tiếp (cần Lock) Vừa (KB/thread) I/O-bound, GUI, mix với thư viện C release GIL.
Multiprocessing Có thật sự Không (mỗi process 1 GIL riêng) Phải IPC (Queue/Pipe/shared mem) Cao (MB/process, startup chậm) CPU-bound: tính toán số, xử lý ảnh, ML batch.
Asyncio Không (single-threaded) Không (chỉ 1 thread) Trực tiếp (không lock vì single-threaded) Rất thấp (KB/coroutine) I/O-bound quy mô lớn: web server, scraper, chatbot, hàng ngàn kết nối.
💡 Cây quyết định nhanh
  1. Code là CPU-bound? → multiprocessing hoặc thư viện C (NumPy).
  2. Code I/O-bound nhẹ (vài chục connection)? → threading (đơn giản hơn) hoặc asyncio.
  3. Code I/O-bound nặng (hàng trăm/ngàn connection đồng thời)? → asyncio.
  4. Cần cả I/O + CPU? → asyncio + loop.run_in_executor(ProcessPoolExecutor(), cpu_func).

4. Coroutine — đơn vị async cơ bản

Trong Python, coroutine là một function được khai báo bằng async def. Khi gọi coroutine function, bạn không chạy nó — bạn nhận được một coroutine object sẵn sàng được schedule.

Coroutine cơ bản
import asyncio

async def fetch(url):
    # giả lập I/O
    await asyncio.sleep(1)
    return f"data from {url}"

# Gọi fetch(...) KHÔNG chạy code bên trong:
coro = fetch("https://example.com")
print(coro)
# <coroutine object fetch at 0x...>
# Phải schedule bằng await, asyncio.run, hoặc create_task

result = asyncio.run(coro)
print(result)  # 'data from https://example.com'
🔥 Gọi coroutine mà không await
async def work():
    print("đang làm")

async def main():
    work()           # SAI — chỉ tạo coro, không chạy
    await work()     # ĐÚNG — schedule và đợi

Python 3.8+ sẽ warning: RuntimeWarning: coroutine 'work' was never awaited. Đây là một trong những bug phổ biến nhất với người mới — luôn kiểm tra mọi async def đều có một await tương ứng đâu đó.

5. await — tạm dừng và nhường quyền

await <awaitable> là biểu thức làm 3 việc:

  1. Tạm dừng coroutine hiện tại.
  2. Nhường quyền điều khiển về event loop để task khác có cơ hội chạy.
  3. Khi awaitable hoàn thành, tiếp tục coroutine từ chỗ tạm dừng với giá trị trả về.

await chỉ được phép dùng bên trong async def. Trong sync function thông thường sẽ là SyntaxError.

await trace
import asyncio, time

async def say(name, delay):
    print(f"{name} bắt đầu")
    await asyncio.sleep(delay)
    print(f"{name} xong")
    return name

async def main():
    t0 = time.perf_counter()
    a = await say("A", 1)   # dừng 1s, sau đó chạy tiếp
    b = await say("B", 2)   # dừng 2s nữa
    print(f"Tổng: {time.perf_counter()-t0:.1f}s")

asyncio.run(main())
"""
A bắt đầu
A xong
B bắt đầu
B xong
Tổng: 3.0s   # tuần tự
"""

Ở ví dụ trên, A và B chạy tuần tự vì mỗi await chờ xong mới chuyển sang dòng tiếp theo. Muốn song song phải dùng gather hoặc create_task — sẽ trình bày bên dưới.

6. asyncio.run — entry point

asyncio.run(coro) là cách khuyến nghị để khởi động ứng dụng asyncio ở top-level (script, file __main__). Nó:

  • Tạo một event loop mới.
  • Chạy coro cho đến khi xong.
  • Đóng tất cả async generator, shutdown executor, đóng loop.
  • Trả về kết quả của coro.
Pattern top-level
import asyncio

async def main():
    # Toàn bộ code async sống ở đây
    print("hello async")

if __name__ == "__main__":
    asyncio.run(main())
🔥 Không gọi asyncio.run từ trong coroutine
async def main():
    asyncio.run(other())   # SAI — RuntimeError

asyncio.run tự tạo loop mới. Nếu bạn đang ở trong coroutine nghĩa là đã có loop chạy — gọi asyncio.run sẽ raise "asyncio.run() cannot be called from a running event loop". Trong coroutine, dùng await hoặc create_task để chạy coro khác.

Trong các framework tích hợp sẵn loop (FastAPI, aiohttp.web, Jupyter, Django async views), bạn không gọi asyncio.run — framework đã quản lý loop. Chỉ dùng nó cho script/CLI tự viết.

7. asyncio.gather — chạy song song

asyncio.gather(*aws) nhận nhiều awaitable và schedule chúng cùng lúc, trả về một awaitable mới. Khi await nó, bạn nhận được danh sách kết quả theo đúng thứ tự đầu vào.

gather — song song
import asyncio, time

async def fetch(name, delay):
    await asyncio.sleep(delay)
    return f"<{name}>"

async def main():
    t0 = time.perf_counter()
    results = await asyncio.gather(
        fetch("A", 1),
        fetch("B", 2),
        fetch("C", 3),
    )
    print(results, f"{time.perf_counter()-t0:.1f}s")
    # ['<A>', '<B>', '<C>'] 3.0s  (chứ không phải 6.0s)

asyncio.run(main())

Lưu ý: gather nhận awaitable trực tiếp làm tham số, không nhận list. gather([c1, c2]) sẽ không chạy mà raise lỗi. Muốn unpack list dùng dấu sao: gather(*[c1, c2]).

Unpack list coroutine
urls = ["u1", "u2", "u3"]
coros = [fetch(u, 1) for u in urls]
results = await asyncio.gather(*coros)
📘 Xử lý lỗi trong gather

Mặc định, nếu một awaitable raise exception, các awaitable khác tiếp tục chạy nhưng gather sẽ raise exception đó ngay khi được await. Hai tham số quan trọng:

  • return_exceptions=True: thay vì raise, exception sẽ được trả về như một phần tử của list kết quả (giống Promise.allSettled của JS).
  • Cancel hành vi: trong Python 3.11+, có asyncio.TaskGroup với structured concurrency — khi một task fail, các task anh em sẽ được cancel tự động.
async def might_fail(x):
    if x < 0: raise ValueError("âm")
    return x * 2

results = await asyncio.gather(
    might_fail(1), might_fail(-1), might_fail(3),
    return_exceptions=True,
)
# [2, ValueError('âm'), 6]

8. asyncio.create_task — fire-and-forget

gather tốt khi bạn biết trước danh sách coroutine cần đợi. Khi muốn schedule một coroutine chạy nềnkhông chờ ngay, dùng asyncio.create_task(coro):

  • Trả về một Task object (subclass của Future).
  • Task bắt đầu chạy ở lần await tiếp theo của coroutine hiện tại.
  • Có thể await task sau để lấy kết quả, hoặc task.cancel() để huỷ.
create_task — chạy nền
import asyncio

async def heartbeat():
    while True:
        print("ping")
        await asyncio.sleep(1)

async def main():
    bg = asyncio.create_task(heartbeat())   # start nền
    try:
        await do_work()                          # làm việc chính
    finally:
        bg.cancel()                                # dọn task nền
        try:
            await bg
        except asyncio.CancelledError:
            pass
🔀 create_task vs await coro trực tiếp
  • await coro: chạy ngay, block coroutine cha đến khi xong. Tuần tự với code phía trước/sau.
  • task = asyncio.create_task(coro): schedule chạy nền, không block. Sau đó có thể await task để chờ, hoặc bỏ qua.
  • asyncio.gather(c1, c2): schedule cả hai chạy song song chờ luôn — tương đương create_task mỗi cái rồi await từng cái.
🔥 Đừng tạo task rồi vứt reference
# SAI — Python có thể GC task chưa xong
asyncio.create_task(background_job())

# ĐÚNG — giữ tham chiếu
task = asyncio.create_task(background_job())
# hoặc lưu vào set:
_bg_tasks: set = set()
t = asyncio.create_task(background_job())
_bg_tasks.add(t)
t.add_done_callback(_bg_tasks.discard)

Python warning: "Task was destroyed but it is pending!". Lý do: event loop chỉ giữ weakref tới task, nếu user không giữ reference mạnh, garbage collector có thể thu hồi giữa chừng.

9. asyncio.wait_for — timeout

Cuộc sống async đầy rủi ro: một request HTTP có thể treo mãi, một database query có thể tắc. Để áp dụng timeout cho một coroutine, dùng asyncio.wait_for(aw, timeout):

wait_for — timeout coroutine
import asyncio

async def slow():
    await asyncio.sleep(10)
    return "done"

async def main():
    try:
        result = await asyncio.wait_for(slow(), timeout=2)
    except asyncio.TimeoutError:
        print("quá hạn 2s")
        result = None
    return result

asyncio.run(main())

Khi hết thời gian, wait_for sẽ cancel coroutine bên trong rồi raise TimeoutError. Quan trọng: coroutine bên trong phải "co-operative" — nó cần đến điểm await để nhận tín hiệu cancel. Nếu nó đang block CPU (vòng for tính toán nặng) thì timeout không tới được.

📘 Python 3.11+ — asyncio.timeout
async with asyncio.timeout(2):
    result = await slow()
# Pythonic hơn, có thể bao nhiều await

Cú pháp context manager đẹp hơn, có thể wrap nhiều dòng await trong cùng một timeout, và chuyển đổi giữa "extend timeout" / "fast cancel" dễ hơn.

10. asyncio.Queue — Producer–Consumer

asyncio.Queue là FIFO queue an toàn cho coroutine, không cần lock (vì single-threaded). Đây là cách kinh điển để tách producer (đẩy việc) và consumer (xử lý):

Producer–Consumer
import asyncio, random

async def producer(q: asyncio.Queue):
    for i in range(10):
        item = f"item-{i}"
        await q.put(item)
        print(f"đẩy {item}")
        await asyncio.sleep(random.uniform(0.1, 0.3))
    await q.put(None)  # sentinel báo hết

async def consumer(q: asyncio.Queue, name: str):
    while True:
        item = await q.get()
        if item is None:
            await q.put(None)  # forward cho consumer khác
            break
        print(f"{name} xử lý {item}")
        await asyncio.sleep(0.5)
        q.task_done()

async def main():
    q = asyncio.Queue(maxsize=5)
    await asyncio.gather(
        producer(q),
        consumer(q, "c1"),
        consumer(q, "c2"),
    )

asyncio.run(main())

API quan trọng của Queue:

  • await q.put(item) — đẩy, block nếu đầy (khi có maxsize).
  • await q.get() — lấy, block nếu rỗng.
  • q.put_nowait(item), q.get_nowait() — bản non-blocking, raise nếu không thể.
  • q.task_done() kết hợp với await q.join() — pattern "đợi tất cả item được xử lý xong".
  • asyncio.PriorityQueue, asyncio.LifoQueue — biến thể.

11. async withasync for

Hai cú pháp hỗ trợ context manager và iterator có chứa code async:

11.1. async with — async context manager

Tương tự with, nhưng __enter__/__exit__ được thay bằng phiên bản async __aenter__/__aexit__. Phổ biến nhất: HTTP session, DB connection, file async.

async with
import aiohttp

async def fetch_json(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()
# Session auto-close (close socket pool) khi rời block.

Tự định nghĩa async context manager với contextlib.asynccontextmanager:

asynccontextmanager
from contextlib import asynccontextmanager

@asynccontextmanager
async def db_connection(dsn):
    conn = await connect(dsn)
    try:
        yield conn
    finally:
        await conn.close()

async with db_connection("postgres://...") as db:
    rows = await db.fetch("SELECT 1")

11.2. async for — async iterator

Iterator có __aiter__ trả về self, __anext__ là coroutine trả về phần tử kế (hoặc raise StopAsyncIteration). Một async generator (function vừa có async def vừa có yield) là cách dễ nhất để tạo:

Async generator + async for
import asyncio

async def stream_lines(file):
    while True:
        line = await file.readline()
        if not line: break
        yield line.decode().strip()

async def main():
    async for line in stream_lines(open_socket):
        print(line)

Async iterator thực sự toả sáng khi nguồn dữ liệu tới dần: HTTP streaming, websocket, Kafka consumer, DB cursor.

12. Event Loop của asyncio

Event loop của Python rất giống event loop của JS: single-threaded, chạy task đến khi gặp await, yield, sau đó pick task tiếp theo.

Asyncio event loop — vòng lặp đơn
Ready Queue

FIFO các task sẵn sàng chạy (vừa được tạo, hoặc vừa được "resume" vì I/O xong).

Waiting Set

Các task đang chờ I/O (selector poll: socket, file descriptor) hoặc đang sleep.

Selector

epoll (Linux), kqueue (macOS), IOCP (Windows). Báo loop biết I/O nào sẵn sàng.

Time wheel

Heap các deadline cho asyncio.sleep, wait_for, call_later.

Mỗi tick:
  1. Tính thời gian đến deadline gần nhất trong time wheel.
  2. Selector poll I/O với timeout = thời gian đó.
  3. Đẩy mọi I/O ready callback vào ready queue.
  4. Đẩy mọi deadline đến hạn vào ready queue.
  5. Drain ready queue: chạy tuần tự từng task đến khi nó await hoặc kết thúc.
  6. Quay lại bước 1.
🔀 So với JS event loop
  • Giống: single-threaded, không block, I/O delegate ra OS, callback-driven.
  • Khác: JS có 2 queue (macrotask + microtask) với rule drain micro trước; asyncio đơn giản hơn — chỉ 1 ready queue.
  • Khác: JS event loop "auto-start" ở browser/Node; Python phải gọi asyncio.run tường minh.
  • Khác: Python asyncio.create_task ~ JS Promise.resolve().then ở chỗ schedule không block, nhưng chỉ "thật sự chạy" khi event loop có cơ hội.

13. Sync vs Async — không trộn lung tung

Quy tắc vàng: code sync nặng KHÔNG được phép chạy trong coroutine. Vì asyncio chỉ có một thread, một dòng time.sleep(5) sẽ đóng băng toàn bộ event loop — mọi connection khác phải chờ.

Anti-pattern — block event loop
import asyncio, time

async def bad():
    time.sleep(5)         # SAI — block toàn loop 5 giây

async def good():
    await asyncio.sleep(5) # ĐÚNG — yield, các task khác tự do chạy

Vấn đề là không phải lúc nào cũng có "phiên bản async" tương ứng. Khi buộc phải gọi function sync nặng (CPU hoặc I/O không có phiên bản async), dùng loop.run_in_executor để đẩy ra thread pool:

run_in_executor
import asyncio
from concurrent.futures import ThreadPoolExecutor

def sync_cpu(n):
    # tính toán nặng, không có phiên bản async
    return sum(i * i for i in range(n))

async def main():
    loop = asyncio.get_running_loop()
    # None = dùng default executor (ThreadPoolExecutor)
    result = await loop.run_in_executor(None, sync_cpu, 10_000_000)
    print(result)

    # Python 3.9+: shortcut
    result = await asyncio.to_thread(sync_cpu, 10_000_000)
  • ThreadPoolExecutor phù hợp khi sync function là I/O-bound (network legacy lib, disk).
  • ProcessPoolExecutor phù hợp khi sync function là CPU-bound thuần Python (vì GIL).
  • asyncio.to_thread(fn, *args) là wrapper gọn cho ThreadPool default — dùng từ Python 3.9.
💡 Tự đo "loop lag"

Một mẹo phổ biến để phát hiện block: chạy một coroutine "heartbeat" đo độ trễ giữa 2 lần asyncio.sleep(0.1). Nếu khoảng cách đột nhiên thành 5 giây thay vì 0.1 → có ai đó block loop. Thư viện như aiomonitor, blockbuster tự động hoá việc này.

14. Hệ sinh thái async

asyncio chỉ là core. Sức mạnh thực sự đến từ thư viện async-aware. Một số tên quan trọng năm 2026:

Thư việnLĩnh vựcGhi chú
aiohttp HTTP client + server Server framework đầy đủ, client với ClientSession reuse connection pool.
httpx HTTP client (sync + async) API thân thiện giống requests; cùng class hỗ trợ cả sync và async — phổ biến nhất 2026.
aiofiles File I/O async Wrap file thường vào thread pool — không phải "true async" nhưng đủ dùng.
asyncpg PostgreSQL Driver async thuần — nhanh hơn psycopg2 đáng kể, không thông qua libpq.
aiomysql / asyncmy MySQL Tương tự cho MySQL/MariaDB.
motor MongoDB Bản async chính thức của pymongo.
redis.asyncio Redis Đi kèm redis-py >= 4.2, không cần aioredis riêng nữa.
FastAPI Web framework Async-first, dựa trên starlette + uvicorn; type hint + Pydantic.
anyio / trio Concurrency layer thay thế anyio chạy trên cả asyncio và trio; có TaskGroup mạnh mẽ và structured concurrency.
httpx — fetch nhiều URL song song
import asyncio, httpx

async def fetch(client, url):
    r = await client.get(url)
    return r.status_code, len(r.content)

async def main(urls):
    async with httpx.AsyncClient(timeout=5) as c:
        results = await asyncio.gather(
            *[fetch(c, u) for u in urls],
            return_exceptions=True,
        )
    return results

urls = [f"https://httpbin.org/delay/{i}" for i in range(10)]
asyncio.run(main(urls))

Bài tập

Bài 1 — Fetch song song so với tuần tự

Cài httpx. Viết fetch(url, session) trả về (url, status_code, len(body)). Viết 2 phiên bản:

  • fetch_sequential(urls) — for loop, await từng cái.
  • fetch_parallel(urls) — dùng asyncio.gather.

Đo thời gian với 10 URL https://httpbin.org/delay/1. So sánh.

Đáp án
import asyncio, time, httpx

async def fetch(url, session):
    r = await session.get(url)
    return url, r.status_code, len(r.content)

async def fetch_sequential(urls):
    async with httpx.AsyncClient(timeout=5) as s:
        out = []
        for u in urls:
            out.append(await fetch(u, s))
        return out

async def fetch_parallel(urls):
    async with httpx.AsyncClient(timeout=5) as s:
        return await asyncio.gather(*[fetch(u, s) for u in urls])

async def main():
    urls = ["https://httpbin.org/delay/1"] * 10
    for name, fn in [("seq", fetch_sequential), ("par", fetch_parallel)]:
        t = time.perf_counter()
        await fn(urls)
        print(name, time.perf_counter() - t)
asyncio.run(main())

Sequential ≈ 10 giây (1s × 10). Parallel ≈ 1 giây — chỉ chậm bằng request lâu nhất. Tỉ lệ tăng tốc tuyến tính theo số request đến khi đụng connection pool / OS limit.

Bài 2 — download_all(urls, concurrency=5)

Cho 100 URL nhưng chỉ muốn tối đa concurrency request đồng thời (tránh ban từ server, tránh exhausted file descriptor). Viết function dùng asyncio.Semaphore.

Đáp án
import asyncio, httpx

async def download_all(urls, concurrency=5):
    sem = asyncio.Semaphore(concurrency)
    async with httpx.AsyncClient(timeout=10) as s:
        async def one(u):
            async with sem:
                r = await s.get(u)
                return u, r.status_code

        return await asyncio.gather(*[one(u) for u in urls])

Pattern: Semaphore(N) cho tối đa N task vào section critical đồng thời. Các task khác await ở async with sem cho đến khi có "vé" rỗi. Cách này đơn giản hơn nhiều so với tự viết worker pool.

Bài 3 — with_timeout(coro, seconds)

Viết wrapper with_timeout(coro, seconds): chạy coroutine với giới hạn thời gian, trả về kết quả nếu kịp, hoặc None nếu hết hạn (không raise exception ra ngoài). So sánh với asyncio.wait_for trực tiếp.

Đáp án
import asyncio

async def with_timeout(coro, seconds):
    try:
        return await asyncio.wait_for(coro, timeout=seconds)
    except asyncio.TimeoutError:
        return None

# So sánh:
# asyncio.wait_for RAISE TimeoutError → caller phải try/except.
# with_timeout swallows → caller chỉ check None.
#
# Trade-off: gọn cho caller nhưng mất thông tin (None vs giá trị bình
# thường có thể cũng là None). Cách an toàn hơn:

_SENTINEL = object()

async def with_timeout(coro, seconds, default=_SENTINEL):
    try:
        return await asyncio.wait_for(coro, seconds)
    except asyncio.TimeoutError:
        if default is _SENTINEL: raise
        return default

Bài 4 — Worker pool với asyncio.Queue

Viết hệ thống: 1 producer push URL vào queue, 5 consumer worker lấy URL ra httpx fetch rồi in status. Khi producer hết URL, gửi sentinel None để consumer dừng. Đảm bảo main chỉ kết thúc khi mọi việc xong.

Đáp án
import asyncio, httpx

async def producer(q, urls):
    for u in urls:
        await q.put(u)
    for _ in range(5):
        await q.put(None)  # 1 sentinel/worker

async def worker(name, q, client):
    while True:
        url = await q.get()
        if url is None:
            break
        try:
            r = await client.get(url, timeout=5)
            print(name, url, r.status_code)
        except Exception as e:
            print(name, url, "ERR", e)

async def main(urls):
    q = asyncio.Queue(maxsize=20)
    async with httpx.AsyncClient() as client:
        workers = [
            asyncio.create_task(worker(f"w{i}", q, client))
            for i in range(5)
        ]
        await producer(q, urls)
        await asyncio.gather(*workers)

asyncio.run(main(["https://httpbin.org/get"] * 20))

Lưu ý 2 điều: (1) Cần 5 sentinel để dừng 5 worker, không phải 1; (2) gather(*workers) đảm bảo main chỉ thoát sau khi mọi worker đã kết thúc — quan trọng để session AsyncClient không bị close sớm.

Bài 5 — Bug check: time.sleep trong coroutine

Đoạn code sau "chạy" mất bao lâu? Có gì sai? Fix.

import asyncio, time

async def slow_task(name):
    print(f"{name} start")
    time.sleep(2)
    print(f"{name} done")

async def main():
    await asyncio.gather(slow_task("A"), slow_task("B"), slow_task("C"))

asyncio.run(main())
Đáp án

Mất 6 giây, không phải 2. Vì time.sleep(2)blocking sync — đóng băng cả event loop. Output: "A start, A done, B start, B done, C start, C done" — hoàn toàn tuần tự, không có concurrency nào cả.

Fix bằng asyncio.sleep (có await):

async def slow_task(name):
    print(f"{name} start")
    await asyncio.sleep(2)
    print(f"{name} done")

Output mới: 3 lần "start" gần như cùng lúc, 2 giây sau là 3 lần "done". Tổng ≈ 2 giây.

Nếu buộc phải gọi sync (vd. library chỉ có sync API), dùng asyncio.to_thread:

async def slow_task(name):
    print(f"{name} start")
    await asyncio.to_thread(time.sleep, 2)
    print(f"{name} done")

Quiz

Q1

Cho async def f(): return 1. Gọi f() trả về gì?

Xem đáp án
✓ Đáp án

Một coroutine object (<coroutine object f at 0x...>), KHÔNG phải 1. Code bên trong chưa chạy. Để lấy giá trị: await f() (trong async context) hoặc asyncio.run(f()) (ở top-level).

Q2

Đúng/sai: "GIL khiến threading hoàn toàn vô dụng cho mọi loại CPU-bound."

Xem đáp án
✓ Đáp án

Không hoàn toàn đúng. Đúng với CPU-bound Python thuần. Nhưng nhiều thư viện C (NumPy, SciPy, Pillow, regex của re2...) release GIL trong vòng tính toán C — threading vẫn có thể song song hoá phần đó. Cũng có chế độ free-threaded build (PEP 703) từ Python 3.13, nhưng chưa phổ biến.

Q3

Đoạn asyncio.gather([coro1, coro2, coro3]) có chạy đúng không?

Xem đáp án
✓ Đáp án

Không. gather nhận awaitable trực tiếp làm tham số, không nhận một list. Truyền list sẽ raise TypeError (vì list không phải awaitable). Phải unpack: asyncio.gather(*[coro1, coro2, coro3]).

Q4

Một coroutine object được tạo ra khi nào thì thật sự bắt đầu chạy code bên trong?

Xem đáp án
✓ Đáp án

Khi nó được schedule vào event loop. Ba cách:

  • asyncio.run(coro) — top-level, blocking.
  • await coro — chạy ngay, đợi xong.
  • asyncio.create_task(coro) — schedule chạy nền, không đợi.

Tự thân f() không chạy gì — đó là điểm khác lớn so với function bình thường.

Q5

Có gì khác giữa hai đoạn sau (mỗi fetch_x() mất 1 giây)?

# A
await fetch_a()
await fetch_b()

# B
await asyncio.gather(fetch_a(), fetch_b())
Xem đáp án
✓ Đáp án

(A) tuần tự: fetch_a phải xong rồi mới khởi tạo fetch_b. Tổng ≈ 2 giây.

(B) song song: cả hai coroutine được schedule cùng lúc, mỗi cái await sleep yield cho event loop xử lý cái kia. Tổng ≈ 1 giây.

Đây là bẫy phổ biến nhất với người mới dùng async/await — "tôi viết async rồi sao vẫn chậm?".

Q6

Có thể dùng await trong một function sync (def bình thường) không?

Xem đáp án
✓ Đáp án

Không. SyntaxError: 'await' outside async function. await chỉ dùng được trong async def. Khi code sync cần chờ một coroutine, phải dùng asyncio.run(coro()) (chỉ ở entry-point) hoặc cấu trúc lại để gọi từ bên trong async context.

Q7

So sánh asyncio.create_task(coro) với await coro.

Xem đáp án
✓ Đáp án
  • await coro — schedule và đợi xong ngay trong cùng dòng. Mọi code phía sau sẽ chạy sau khi coro xong. Tuần tự.
  • asyncio.create_task(coro) — schedule chạy nền, trả về Task. Code phía sau chạy song song với task. Có thể await task sau để lấy kết quả, hoặc task.cancel() để huỷ.

Quy tắc: cần kết quả ngay → await. Cần fire-and-forget hoặc song song với việc khác → create_task.

Q8

Một async function gọi time.sleep(1) giữa hai dòng await — gây hậu quả gì?

Xem đáp án
✓ Đáp án

Đóng băng toàn bộ event loop trong 1 giây. Vì time.sleep là sync blocking — nó không yield cho event loop. Mọi coroutine khác (kể cả hàng ngàn client connection đang chờ) đều phải đợi. Ở web server, người dùng sẽ thấy latency tăng vọt.

Fix: dùng await asyncio.sleep(1) (yield cho loop). Nếu code sync nặng không tránh được, đẩy ra thread pool: await asyncio.to_thread(blocking_fn, ...).

Tổng kết

Sau chương 8, bạn nên đã master:

  • Concurrency vs Parallelism: concurrency = interleave (1 CPU đủ), parallelism = chạy thật cùng lúc (nhiều CPU).
  • GIL: rào cản threading CPU-bound trong CPython, nhưng I/O-bound vẫn dùng threading tốt.
  • 4 mô hình: sync (đơn giản), threading (I/O nhẹ), multiprocessing (CPU thật), asyncio (I/O nặng).
  • Coroutine: async def tạo coroutine; gọi không chạy, phải schedule.
  • await: tạm dừng, yield cho event loop, tiếp tục khi awaitable xong. Chỉ trong async function.
  • asyncio.run: entry-point top-level, không gọi từ trong coroutine.
  • gather: chạy nhiều awaitable song song, trả list kết quả theo thứ tự.
  • create_task: schedule nền, trả Task object. Nhớ giữ reference.
  • wait_for / asyncio.timeout: áp dụng timeout, cancel nếu quá hạn.
  • Queue: producer-consumer không cần lock vì single-threaded.
  • async with / async for: context manager + iterator async — cho session, stream, cursor.
  • Event loop asyncio: single-threaded, selector-based, giống JS event loop về tinh thần nhưng đơn giản hơn (1 queue).
  • Sync vs async: KHÔNG block event loop. Sync nặng → run_in_executor / to_thread.
  • Ecosystem: aiohttp, httpx, aiofiles, asyncpg, motor, redis.asyncio, FastAPI, anyio.

Kết nối

  • Chương 6 (Generator) — async generator là sự kết hợp generator + coroutine; async for hoạt động trên async iterator.
  • Chương 7 (Decorators) — decorator có thể wrap coroutine; nhớ check asyncio.iscoroutinefunction để xử lý đúng.
  • Chương 9 (Typing) — type hint cho async: Awaitable[T], Coroutine[Any, Any, T], AsyncIterator[T].
  • Chương 10 (Testing) — test async với pytest-asyncio: marker @pytest.mark.asyncio, fixture async.
  • JavaScript Chương 6 (Async) — đối chiếu event loop JS với asyncio: cùng mental model, nhưng JS có micro/macro queue rõ ràng, JS auto-start loop còn Python cần asyncio.run.
  • Dart Chương 7 (async/await) — Dart có cú pháp async/await rất giống Python; Future ~ Python coroutine; Stream ~ async iterator.
  • Networking (Pillar 4) — asyncio dùng OS selector (epoll/kqueue/IOCP); hiểu TCP/HTTP giúp debug socket leak, connection pool.
  • System Design (Pillar 7) — async I/O là nền tảng cho high-concurrency server (10K+ connection), pattern phổ biến trong microservice và real-time system.