Giao diện
Concurrency Trade-offs — GIL, Threading, Multiprocessing, Asyncio
Năm 2022, một startup fintech tại TP.HCM xây hệ thống thanh toán real-time bằng Python. Team lead quyết định dùng threading để xử lý song song hàng ngàn giao dịch mỗi giây — kỳ vọng rằng 8 cores sẽ cho throughput gấp 8 lần. Kết quả sau khi deploy: hiệu năng không tăng, thậm chí latency p99 tệ hơn 15% so với chạy sequential. Nguyên nhân? Ba chữ cái mà mọi Python engineer phải hiểu trước khi chạm vào concurrency — GIL.
Bài này không dạy bạn syntax của threading hay asyncio. Bài này dạy bạn khi nào dùng cái gì, vì sao, và trade-off thực sự là gì — kiến thức mà chỉ có sau khi bạn đã debug production incidents lúc 3 giờ sáng.
GIL — Con Voi Trong Phòng
🎯 Mục tiêu
- Hiểu GIL là gì và vì sao CPython cần nó
- Phân biệt rõ I/O-bound vs CPU-bound workload
- Nắm decision matrix để chọn đúng concurrency model (threading / multiprocessing / asyncio)
- So sánh trung thực Python vs Go cho concurrency workloads
- Nhận diện anti-patterns phổ biến khi dùng sai concurrency model
Bức tranh tư duy
Hãy tưởng tượng một quán cà phê ở Đà Nẵng có 4 máy pha espresso (4 CPU cores) nhưng chỉ có một chiếc chìa khoá duy nhất để mở tủ nguyên liệu (GIL). Dù có 4 barista (4 threads), mỗi lần chỉ một barista được mở tủ lấy cà phê để pha. Các barista khác phải đứng chờ.
Nhưng — khi một barista đang chờ nước sôi (I/O wait), họ sẽ trả lại chìa khoá cho barista khác. Chỉ khi barista đang xay cà phê bằng tay (CPU computation) thì họ mới giữ chìa khoá liên tục.
Thread A: ──────🔒────────────────🔓──────
Thread B: ──────────🔒────────🔓──────────
Thread C: ────────────────🔒──────🔓──────
↑
GIL: chỉ 1 thread chạy Python bytecode tại 1 thời điểmKhi nào analogy không còn chính xác: GIL thực tế phức tạp hơn — nó có cơ chế "check interval" (mỗi 5ms trong Python 3.2+) để force-release, và C extensions có thể tự release GIL. Nhưng mental model trên đủ chính xác cho 95% quyết định kiến trúc.
Cốt lõi kỹ thuật — GIL bảo vệ gì?
CPython quản lý bộ nhớ bằng reference counting — mỗi object có một biến đếm ob_refcnt. Khi hai thread cùng tăng/giảm biến đếm này mà không có lock, bạn sẽ gặp race condition → memory leak hoặc segfault.
GIL là giải pháp đơn giản nhất: lock toàn cục đảm bảo chỉ một thread chạy Python bytecode tại bất kỳ thời điểm nào.
python
import sys
x = []
# ob_refcnt = 1 (biến x)
print(sys.getrefcount(x)) # 2 (biến x + tham số getrefcount)
# Nếu không có GIL, 2 threads cùng append vào x
# có thể corrupt ob_refcnt → crashBốn sự thật quan trọng về GIL:
| # | Sự thật | Hệ quả |
|---|---|---|
| 1 | GIL bảo vệ reference counting (memory management) | Đây là lý do CPython có GIL — không phải vì "Python chậm" |
| 2 | GIL được release khi I/O (network, disk, database) | Threading hoạt động tốt cho I/O-bound workload |
| 3 | GIL KHÔNG release khi CPU computation (tính toán, parsing) | Threading vô dụng (thậm chí chậm hơn) cho CPU-bound |
| 4 | C extensions có thể tự release GIL | NumPy, OpenCV chạy song song thật sự dù dùng threading |
⚠️ Hiểu lầm phổ biến
"Python không thể chạy song song" là sai. Python threads không chạy song song cho CPU-bound code. Nhưng multiprocessing cho true parallelism, và nhiều C extensions (NumPy, PIL) release GIL khi tính toán nặng.
Threading — Khi I/O Là Bottleneck
Khi nào threading tỏa sáng?
Threading hoạt động xuất sắc khi bottleneck của bạn là chờ đợi — chờ HTTP response, chờ database query, chờ file I/O. Trong lúc thread A chờ response từ API, GIL được release và thread B có thể chạy.
python
import concurrent.futures
import httpx
def fetch_user_data(user_id: int) -> dict:
"""Fetch user từ external API — I/O-bound operation."""
response = httpx.get(f"https://api.example.com/users/{user_id}")
return response.json()
# Sequential: 10 users × 200ms = 2 seconds
# Threaded: 10 users × 200ms / 10 threads ≈ 200ms
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
user_ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
results = list(executor.map(fetch_user_data, user_ids))Tại sao nhanh hơn? Khi httpx.get() chờ response từ server, GIL được release. Thread pool sẽ schedule thread khác chạy ngay lập tức — overlap thời gian chờ của nhiều requests.
Khi nào KHÔNG dùng threading
🔴 Code Smell: Threading cho CPU-bound
python
# ❌ Tệ: threading cho tính toán nặng
import threading
def compute_heavy(n: int) -> int:
"""CPU-bound: GIL KHÔNG release ở đây."""
return sum(i * i for i in range(n))
threads = [
threading.Thread(target=compute_heavy, args=(10_000_000,))
for _ in range(4)
]
for t in threads:
t.start()
for t in threads:
t.join()
# Kết quả: CHẬM HƠN sequential vì GIL + context switching overhead!✅ ĐÚNG — Dùng ProcessPoolExecutor cho CPU-bound
python
import concurrent.futures
def compute_heavy(n: int) -> int:
return sum(i * i for i in range(n))
# Mỗi process có GIL riêng → true parallelism
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
chunks = [10_000_000] * 4
results = list(executor.map(compute_heavy, chunks))
total = sum(results)Thread safety — Đừng quên
Threading có shared memory — nếu nhiều threads cùng ghi vào một data structure mà không có lock, bạn sẽ gặp race condition. GIL bảo vệ CPython internals, không bảo vệ logic của bạn.
python
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock: # ← Luôn dùng lock khi shared state
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
assert counter == 400_000 # ✅ An toàn với lockMultiprocessing — Khi CPU Là Bottleneck
Khi nào multiprocessing tỏa sáng?
Khi workload của bạn là CPU-heavy — tính toán nặng, image processing, ML inference, hash computation — bạn cần true parallelism. Mỗi process có Python interpreter riêng, GIL riêng → chạy song song thật sự trên nhiều CPU cores.
python
import concurrent.futures
import hashlib
def hash_password(password: str) -> str:
"""CPU-intensive: 100K iterations of SHA-256."""
result = password.encode()
for _ in range(100_000):
result = hashlib.sha256(result).digest()
return result.hex()
# Mỗi process có GIL riêng → true parallelism
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
passwords = ["pass1", "pass2", "pass3", "pass4"]
hashes = list(executor.map(hash_password, passwords))Trade-offs thực tế
Multiprocessing không miễn phí. Mỗi lựa chọn kiến trúc đều có cái giá:
| Trade-off | Chi tiết | Impact |
|---|---|---|
| Process startup | ~50-100ms mỗi process | Không phù hợp cho task chạy <100ms |
| Memory duplication | Mỗi process copy data riêng | 4 workers × 500MB data = 2GB RAM |
| IPC complexity | Dữ liệu phải serialize/deserialize (pickle) | Không gửi được socket, file handle, lock |
| Debugging khó hơn | Mỗi process có PID riêng, log riêng | Cần centralized logging |
Best for: batch processing, ML inference pipeline, image/video processing, password hashing, data transformation lớn.
💡 Mẹo thực chiến
Dùng initializer parameter trong ProcessPoolExecutor để load model/data một lần per worker thay vì mỗi task:
python
import concurrent.futures
_model = None
def init_worker():
global _model
_model = load_heavy_ml_model() # Chỉ load 1 lần per process
def predict(input_data: dict) -> dict:
return _model.predict(input_data)
with concurrent.futures.ProcessPoolExecutor(
max_workers=4,
initializer=init_worker,
) as executor:
results = list(executor.map(predict, batch_inputs))Asyncio — Khi Cần Hàng Ngàn Connections
Khi nào asyncio tỏa sáng?
Khi bạn cần xử lý hàng ngàn I/O connections đồng thời trên một thread duy nhất — web servers, websocket connections, API gateways, web crawlers. Asyncio là cooperative multitasking: một thread, nhiều coroutines, không có GIL issue vì chỉ có một thread.
python
import asyncio
import httpx
async def fetch_user(client: httpx.AsyncClient, user_id: int) -> dict:
"""Fetch một user — await để nhường event loop."""
response = await client.get(f"/users/{user_id}")
return response.json()
async def fetch_all_users(user_ids: list[int]) -> list[dict]:
"""Fetch tất cả users đồng thời."""
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
tasks = [fetch_user(client, uid) for uid in user_ids]
return await asyncio.gather(*tasks)
# 1000 concurrent requests trên một single thread!
results = asyncio.run(fetch_all_users(range(1, 1001)))Key insight: Asyncio không tạo thread mới. Nó chạy event loop trên một thread duy nhất, và mỗi await là một "điểm nhường" — khi coroutine A chờ I/O, event loop chuyển sang coroutine B. Không context switching overhead, không GIL contention.
Trade-offs thực tế
| Trade-off | Chi tiết | Impact |
|---|---|---|
| Async ecosystem bắt buộc | Phải dùng httpx, aiohttp, asyncpg — không dùng requests, psycopg2 | Không thể mix sync/async tùy tiện |
| Một blocking call = đóng băng | time.sleep(5) freeze toàn bộ event loop | Phải dùng asyncio.sleep(), await mọi I/O |
| Debug khó hơn | Stack traces dài hơn, exception propagation phức tạp | Cần asyncio.create_task() + proper error handling |
| Learning curve | async/await syntax đơn giản, nhưng mental model phức tạp | Dễ viết code "trông async" nhưng thực chất blocking |
Best for: FastAPI/Starlette endpoints, websocket servers, API gateways, web crawlers, chat applications.
Blocking call — kẻ thù số 1 của asyncio
python
import asyncio
import time
async def bad_handler():
"""❌ KHÔNG BAO GIỜ làm thế này trong async context."""
time.sleep(5) # Blocking! Freeze toàn bộ event loop 5 giây
return {"status": "done"}
async def good_handler():
"""✅ Dùng asyncio.sleep hoặc run_in_executor cho blocking ops."""
await asyncio.sleep(5) # Non-blocking — event loop tiếp tục serve
return {"status": "done"}
async def acceptable_handler():
"""✅ Wrap blocking call trong executor nếu bắt buộc phải dùng sync lib."""
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, # default ThreadPoolExecutor
sync_legacy_function, # blocking function
)
return resultDecision Matrix — Chọn Đúng Mô Hình
Đây là bảng quyết định mà bạn nên bookmark — nó sẽ cứu bạn khỏi hàng giờ refactor sai model:
| Workload | Model | Vì sao |
|---|---|---|
| 10 API calls song song | threading | Đơn giản, I/O-bound, concurrency thấp |
| 10K websocket connections | asyncio | Concurrency cao, I/O-bound, memory hiệu quả |
| Image processing batch | multiprocessing | CPU-bound, cần true parallelism |
| FastAPI endpoint | asyncio (built-in) | Framework đã handle event loop |
| ML inference pipeline | multiprocessing | CPU-bound, isolate workers, tận dụng nhiều cores |
| Background email sending | threading hoặc asyncio | I/O-bound, concurrency vừa phải |
| Data pipeline ETL | multiprocessing | CPU-bound transform, large data chunks |
| Real-time chat server | asyncio | Hàng ngàn connections, low latency |
Flowchart quyết định
Workload của bạn là gì?
│
├─ Chủ yếu chờ I/O (network, disk, DB)?
│ ├─ Cần < 100 concurrent tasks?
│ │ └─ ✅ threading (đơn giản nhất)
│ └─ Cần > 100 concurrent tasks?
│ └─ ✅ asyncio (memory hiệu quả)
│
├─ Chủ yếu tính toán CPU?
│ └─ ✅ multiprocessing (true parallelism)
│
└─ Cả hai (I/O + CPU)?
└─ ✅ multiprocessing + asyncio trong mỗi workerKhi Nào Go Thắng Python — Honest Comparison
Go goroutines vs Python — so sánh công bằng
Một kỹ sư trưởng thực thụ không chỉ biết Python — họ biết khi nào Python không phải lựa chọn tốt nhất. Đây là so sánh trung thực:
Go: goroutine = lightweight (2KB stack) + no GIL + preemptive scheduling
Python: thread = heavyweight (8MB stack) + GIL + OS scheduling
coroutine = lightweight + single thread + cooperative scheduling| Tiêu chí | Go goroutines | Python asyncio | Python threading |
|---|---|---|---|
| Memory per unit | ~2KB (goroutine) | ~1KB (coroutine) | ~8MB (OS thread) |
| Scheduling | Preemptive (Go runtime) | Cooperative (await) | Preemptive (OS) |
| GIL | Không có | Không ảnh hưởng (1 thread) | Bị giới hạn |
| Max concurrent | Hàng triệu | Hàng trăm ngàn | Hàng ngàn |
| CPU parallelism | Built-in (GOMAXPROCS) | Không (1 thread) | Không (GIL) |
| Deployment | Single binary | Python runtime + deps | Python runtime + deps |
| Latency | Rất thấp (compiled) | Thấp (interpreted) | Trung bình |
Case study: Penrift Tunnel
Hệ thống tunnel networking cần quản lý hàng ngàn TCP connections đồng thời, mỗi connection thực hiện I/O liên tục. Go xử lý bài toán này tự nhiên với goroutines:
- Memory: 10K goroutines ≈ 20MB. 10K Python threads ≈ 80GB (không khả thi)
- Python asyncio cũng handle được (10K coroutines ≈ 10MB), nhưng Go có lợi thế: compiled binary → latency thấp hơn, deployment đơn giản hơn, goroutine leak detection built-in
- Kết luận thực tế: infrastructure networking tools → Go. Business logic APIs → Python
Góc nhìn kỹ sư trưởng
💡 Nhận định từ Giáo sư Tom
Python cho business logic + APIs (FastAPI, data processing, ML serving). Go cho infrastructure + networking tools (proxies, tunnels, load balancers, CLI tools). Hai ngôn ngữ bổ sung cho nhau, không thay thế nhau.
Đừng rewrite FastAPI service sang Go chỉ vì "Go nhanh hơn". Đừng viết networking proxy bằng Python chỉ vì "team quen Python". Chọn đúng tool cho đúng job.
📌 Performance note
Trước khi chọn concurrency model, hãy profile. Nếu 90% thời gian là database query, threading đơn giản có thể đủ. Đừng dùng asyncio chỉ vì nó "cool". Đừng dùng multiprocessing nếu workload chỉ là fetch API. Đo lường trước, quyết định sau.
Bài Tập Nhanh
Bài 1: Match workload → concurrency model
Cho 3 workloads sau, hãy chọn concurrency model phù hợp nhất:
🧠 Quiz
Câu 1: Web scraper cần crawl 500 URLs
- [x] A)
asyncio— I/O-bound, high concurrency, memory hiệu quả - [ ] B)
multiprocessing— cần true parallelism - [ ] C)
threading— đơn giản nhất - [ ] D) Sequential — không cần concurrency
💡 Giải thích: 500 concurrent connections là high concurrency I/O-bound workload.
asynciovớihttpx.AsyncClientsẽ handle hiệu quả trên single thread.threadingcũng được nhưng 500 OS threads tốn memory.
🧠 Quiz
Câu 2: Resize 1000 ảnh từ 4K xuống thumbnail
- [ ] A)
asyncio— non-blocking I/O - [x] B)
multiprocessing— CPU-bound, true parallelism - [ ] C)
threading— song song hóa I/O - [ ] D)
threading+asynciokết hợp
💡 Giải thích: Image resizing là CPU-bound computation (pixel manipulation). GIL sẽ ngăn threading chạy song song.
multiprocessingcho mỗi worker một GIL riêng → true parallelism trên nhiều cores.
🧠 Quiz
Câu 3: Gửi 50 emails qua SMTP server
- [ ] A)
asyncio— cần non-blocking - [ ] B)
multiprocessing— cần parallelism - [x] C)
threading— I/O-bound, low concurrency, đơn giản nhất - [ ] D) Sequential — 50 emails không đáng song song hóa
💡 Giải thích: Gửi email là I/O-bound (chờ SMTP server respond). 50 tasks là low concurrency →
threadingvớiThreadPoolExecutorlà giải pháp đơn giản, dễ debug nhất.asynciocũng được nhưng overkill cho 50 tasks.
Bài 2: Tìm blocking call trong asyncio
🧠 Quiz
Câu 4: Đoạn code asyncio nào có vấn đề?
python
import asyncio
import requests # ← Chú ý library
import json
async def get_weather(city: str) -> dict:
response = requests.get(f"https://api.weather.com/{city}") # Line A
data = response.json() # Line B
await asyncio.sleep(0) # Line C
return data
async def main():
tasks = [get_weather(city) for city in ["hanoi", "hcm", "danang"]]
results = await asyncio.gather(*tasks) # Line D- [x] A) Line A —
requests.get()là blocking call, freeze event loop - [ ] B) Line B —
.json()là CPU-bound - [ ] C) Line C —
asyncio.sleep(0)vô nghĩa - [ ] D) Line D —
asyncio.gatherkhông hoạt động với sync functions
💡 Giải thích:
requestslà sync library —requests.get()block toàn bộ event loop cho đến khi nhận response. Dù cóasync defvàawait asyncio.gather(), các tasks sẽ chạy sequential vì không cóawaitnào trongget_weathernhường event loop trước khi request hoàn thành. Fix: dùnghttpx.AsyncClientvớiawait client.get().
Production Anti-Pattern
🔥 Anti-pattern: "Asyncio All The Things"
Tình huống thực tế: Team backend quyết định rewrite toàn bộ Django monolith sang FastAPI async. Kết quả sau 3 tháng:
Vấn đề 1 — sync_to_async wrapper mọi nơi
python
# ❌ Wrap sync ORM call — chỉ chuyển blocking sang thread pool
from asgiref.sync import sync_to_async
@sync_to_async
def get_user(user_id: int):
return User.objects.get(id=user_id) # Django ORM vẫn blocking bên trong→ Không có performance gain. Chỉ thêm overhead của thread pool.
Vấn đề 2 — Một requests.get() freeze event loop
python
async def enrich_user(user: dict) -> dict:
# ❌ Quên rằng requests là sync library
profile = requests.get(f"https://api.linkedin.com/profile/{user['id']}")
return {**user, "profile": profile.json()}→ Event loop đóng băng 2-5 giây mỗi request. Toàn bộ server ngừng serve.
Vấn đề 3 — Fire-and-forget tasks không error handling
python
async def handle_order(order: dict):
# ❌ Task thất bại sẽ bị nuốt im lặng
asyncio.create_task(send_notification(order))
asyncio.create_task(update_inventory(order))
return {"status": "accepted"}→ send_notification throw exception → không ai biết → customer không nhận email → mất khách.
✅ ĐÚNG — Nguyên tắc async thực chiến
Fix 1: Chỉ dùng async ở nơi concurrency thực sự cần thiết
python
# Sync cho logic đơn giản
def calculate_total(items: list[dict]) -> float:
return sum(item["price"] * item["qty"] for item in items)
# Async chỉ cho I/O concurrent
async def fetch_prices(product_ids: list[int]) -> list[float]:
async with httpx.AsyncClient() as client:
tasks = [client.get(f"/prices/{pid}") for pid in product_ids]
responses = await asyncio.gather(*tasks)
return [r.json()["price"] for r in responses]Fix 2: Luôn handle errors cho background tasks
python
async def handle_order(order: dict):
# ✅ Proper error handling cho fire-and-forget tasks
task = asyncio.create_task(send_notification(order))
task.add_done_callback(_handle_task_error)
return {"status": "accepted"}
def _handle_task_error(task: asyncio.Task):
if task.exception():
logger.error(f"Background task failed: {task.exception()}")
# Alert, retry, hoặc fallbackFix 3: Wrap blocking calls đúng cách khi bắt buộc dùng sync library
python
import asyncio
import functools
async def enrich_user(user: dict) -> dict:
loop = asyncio.get_event_loop()
# ✅ Chạy blocking call trong thread pool, không block event loop
profile = await loop.run_in_executor(
None,
functools.partial(requests.get, f"https://api.linkedin.com/profile/{user['id']}")
)
return {**user, "profile": profile.json()}⚠️ Quy tắc vàng
Đừng chuyển sang async vì "trend". Chỉ dùng async khi bạn có bằng chứng rằng I/O concurrency là bottleneck. Giữ synchronous code cho mọi thứ đơn giản — dễ đọc, dễ debug, dễ test hơn gấp 10 lần.
Tổng Kết
| Concurrency Model | Khi nào dùng | Khi nào tránh | Memory | Complexity |
|---|---|---|---|---|
threading | I/O-bound, < 100 tasks | CPU-bound | Cao (8MB/thread) | Thấp |
multiprocessing | CPU-bound, true parallelism | Nhẹ I/O, cần shared state | Rất cao (copy data) | Trung bình |
asyncio | I/O-bound, > 100 connections | CPU-bound, blocking libs | Thấp (~1KB/coroutine) | Cao |
| Go goroutines | Infrastructure, networking | Khi team chỉ biết Python | Rất thấp (2KB) | Trung bình |
Nguyên tắc cuối cùng: Profile trước, quyết định sau. Concurrency model sai sẽ tốn hàng tuần refactor. Concurrency model đúng sẽ cứu bạn hàng giờ debug lúc 3 giờ sáng.