Python Concurrency: Hiểu GIL, Threading và Multiprocessing từ gốc
Hướng dẫn chuyên sâu về Python Concurrency: Hiểu bản chất GIL, phân biệt Threading vs Multiprocessing, tối ưu I/O vs CPU-bound và tư duy kiến trúc backend.

Nếu bạn mới ra trường và bắt đầu bước chân vào thế giới Python backend, sớm muộn gì bạn cũng sẽ va phải ba từ khóa kinh điển:
Threading, Multiprocessing và GIL.
Và ngay sau đó là một ma trận những câu hỏi gây hoang mang trong các buổi phỏng vấn kỹ thuật:
“Python có GIL thì threading có chạy song song thật sự được không?”
“Nếu threading bị GIL kìm hãm, sao không dùng multiprocessing cho tất cả mọi thứ?”
“Một Process có tương ứng với một CPU Core không?”
“Nếu Python 3.13+ bỏ GIL (Free-threaded), multiprocessing có trở nên vô dụng không?”
Câu trả lời sách giáo khoa thường chỉ quẳng cho bạn định nghĩa: “GIL là Global Interpreter Lock ngăn nhiều thread thực thi bytecode cùng lúc.” Nhưng định nghĩa đó không giúp bạn thiết kế được một hệ thống backend xử lý hàng nghìn request mỗi giây.
Bài viết này sẽ tháo dỡ toàn bộ cơ chế concurrency của Python từ cấp độ phần cứng CPU Core, hệ điều hành (OS), cho đến CPython runtime để xây dựng một mô hình tư duy (mental model) chuẩn xác nhất.
Phần 1: Nền tảng (Mô hình tư duy)
Để không bao giờ bị rối, bạn phải phân biệt rạch ròi ba khái niệm: Process, Thread và CPU Core.
1. Process vs. Thread vs. CPU Core
- Process (Tiến trình): Một chương trình đang chạy, có không gian bộ nhớ riêng và một CPython interpreter riêng. Process là thực thể cha — nó có thể sinh ra nhiều Thread con bên trong.
- Thread (Luồng): Luồng thực thi lệnh nằm bên trong một Process cha. Các Thread trong cùng Process dùng chung bộ nhớ heap của Process đó. Một Process có thể sinh ra
nThread con. - CPU Core (Lõi vi xử lý): Phần cứng vật lý thực sự chạy lệnh. Hệ điều hành (OS Scheduler) phân phối Thread và Process lên các Core để thực thi.
Quy tắc nền tảng: Process KHÔNG phải là CPU Core.
Process là khái niệm quản lý của hệ điều hành; Core là phần cứng. OS mới là bên quyết định Core nào chạy Thread nào, tại từng thời điểm.
Mô hình Threading — 1 Process cha, n Thread con, n Core:
Khi bạn dùng threading.Thread(...), Python Process hiện tại đóng vai trò cha và sinh ra các Thread con bên trong. OS Scheduler tự do phân phối mỗi Thread lên một CPU Core khác nhau cùng lúc.
Lưu ý (sẽ mổ xẻ ở Phần 2): OS thật sự đặt các Thread lên các Core khác nhau — nhưng trong CPython chuẩn, GIL chỉ cho phép một Thread chạy Python bytecode tại mỗi thời điểm. Vì vậy các Core dư chỉ có lợi cho tác vụ chờ I/O và C-extension, không có lợi cho tính toán thuần Python.
Mô hình Multiprocessing — n Process cha, n Core:
Khi bạn dùng multiprocessing.Process(...), bạn khởi động n Process độc lập — mỗi cái có bộ nhớ riêng và GIL riêng. OS Scheduler phân phối n Process đó lên n CPU Core song song hoàn toàn.
2. GIL là gì? Chiếc Micro duy nhất trong phòng họp
Nếu OS cho phép các thread chạy trên nhiều core, tại sao CPython lại không làm được điều đó cho code tính toán?
Đó là vì GIL (Global Interpreter Lock).
Hãy tưởng tượng một phòng họp có các diễn giả (Threads) ngồi quanh bàn. Tất cả đều muốn phát biểu (thực thi Python bytecode). Nhưng trong phòng chỉ có duy nhất một chiếc Micro (GIL).
Bất kỳ ai muốn nói đều phải cầm được chiếc Micro. Dù bạn có nhiều cái loa (CPU Cores) gắn ở các góc phòng, tại một thời điểm chỉ có tiếng của người cầm Micro được phát ra.
3. Tại sao GIL lại tồn tại?
GIL không phải là một lỗi (bug); nó là một quyết định thiết kế (design decision) từ những năm 1990 của Guido van Rossum:
- Quản lý bộ nhớ đơn giản bằng Reference Counting: CPython theo dõi số lượng tham chiếu trỏ tới mỗi object (
PyObject->ob_refcnt). Nếu nhiều thread cùng tăng/giảm biến đếm này mà không có khóa, bộ nhớ sẽ bị rò rỉ (memory leak) hoặc bị giải phóng sớm (dangling pointer gây crash). - C-Extension cực nhanh và dễ viết: Các thư viện C/C++ tích hợp vào Python không cần phải lo lắng về việc lock từng struct nhỏ, giúp hệ sinh thái C-extension của Python bùng nổ (NumPy, SciPy).
- Tốc độ đơn luồng (Single-thread performance): Không có overhead từ hàng nghìn fine-grained locks li ti, code đơn luồng của Python chạy nhanh hơn rất nhiều.
Phần 2: Điều tra (CPU-bound vs. I/O-bound)
Hiểu được GIL rồi, giờ hãy trả lời câu hỏi thực tế: Vậy Threading trong Python có vô dụng không?
Câu trả lời là: Hoàn toàn không. Threading cực kỳ mạnh mẽ cho I/O-bound.
1. I/O-bound: Khi GIL tự nguyện “nhường bước”
Hãy xem một kịch bản backend điển hình: Gọi 100 API của bên thứ ba hoặc thực hiện 100 câu query vào PostgreSQL.
Trong phần lớn thời gian chờ mạng (200ms) hoặc chờ đĩa, CPU hoàn toàn rảnh rỗi. CPython được lập trình để: Ngay trước khi thực hiện các tác vụ I/O (network read/write, file I/O, time.sleep()), Thread hiện tại sẽ chủ động GIẢI PHÓNG (RELEASE) GIL.
Nhờ cơ chế này, trong lúc Thread 1 đang chờ dữ liệu mạng từ socket, các thread khác tranh thủ lấy GIL và gửi request tiếp theo. Tổng thời gian hoàn thành 100 requests giảm từ 20 giây xuống còn 0.3 giây!
2. Thử nghiệm Benchmark: Số liệu không biết nói dối
Hãy chạy một thực nghiệm so sánh giữa ba tác vụ trên máy 8-Core:
import time
import requests
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 1. Tác vụ I/O-bound (Gọi HTTP)
def io_task(url):
resp = requests.get(url)
return len(resp.content)
# 2. Tác vụ CPU-bound (Tính toán nặng)
def cpu_task(n):
count = 0
for i in range(n):
count += i * i
return count
Kết quả benchmark thực tế khi xử lý 10 tác vụ:
| Chiến lược | 10 Tác vụ I/O-bound (Gọi API 500ms) | 10 Tác vụ CPU-bound (100M phép tính) |
|---|---|---|
| Tuần tự (Single-thread) | ~ 5.20 giây | ~ 12.40 giây |
10 Threads (ThreadPoolExecutor) |
~ 0.58 giây (Nhanh gấp 9x) 🚀 | ~ 12.80 giây (Chậm hơn do tranh chấp GIL!) 🔴 |
10 Processes (ProcessPoolExecutor) |
~ 0.65 giây (Tốn RAM hơn) | ~ 2.10 giây (Nhanh gấp 6x nhờ 8 Cores) ⚡ |
Bài học rút ra:
- Với I/O-bound: Threading giải quyết bài toán xuất sắc mà không tốn nhiều RAM.
- Với CPU-bound: Threading vô tác dụng vì GIL. Bạn phải dùng Multiprocessing (hoặc C-extensions/Rust) để phân phối sang các core khác.
Phần 3: Chẩn đoán (Các cạm bẫy & Sai lầm thường gặp)
Cạm bẫy 1: “Tại sao không dùng Multiprocessing cho mọi thứ?”
Nhiều lập trình viên nghĩ: “Nếu Multiprocessing vừa tận dụng được nhiều Core cho CPU-bound lại vừa chạy được song song, sao không dẹp Threading đi cho khỏe?”
Đây là cái giá đắt đỏ bạn phải trả khi dùng multiprocessing:
- Overhead cấp phát bộ nhớ khổng lồ: Mỗi process mới là một phiên bản sao chép đầy đủ của CPython runtime, nạp lại toàn bộ thư viện dependencies. 100 threads tốn ~20MB RAM; 100 processes có thể “ngốn” 4GB - 8GB RAM và làm sập server vì Out Of Memory (OOMKilled).
- Chi phí IPC & Serialization (Pickling Overhead): Vì các process có vùng nhớ độc lập, dữ liệu truyền giữa các process (
multiprocessing.Queue,Pipe) bắt buộc phải được chuyển thành chuỗi byte (Pickle/Serialize) ở Process gửi rồi giải mã (Unpickle/Deserialize) ở Process nhận. Với các object dữ liệu lớn (như DataFrame hoặc mảng ảnh), thời gian serialize có thể còn lâu hơn thời gian tính toán!
Cạm bẫy 2: “Python có GIL rồi nên code mặc định là Thread-safe!”
Đây là sai lầm nguy hiểm nhất của các lập trình viên chuyển sang viết Python đa luồng.
GIL chỉ bảo vệ tính toàn vẹn nội bộ của CPython interpreter (tránh crash C code), nó KHÔNG bảo vệ tính toàn vẹn của logic ứng dụng của bạn.
CPython chuyển đổi luồng (thread switch) sau mỗi 100 bytecode instructions hoặc sau một khoảng thời gian (sys.getswitchinterval(), mặc định 5ms).
Hãy xem đoạn code sau:
import threading
counter = 0
def increase():
global counter
for _ in range(100_000):
counter += 1 # ❌ KHÔNG HỀ THREAD-SAFE!
threads = [threading.Thread(target=increase) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Final Counter: {counter}")
# Kết quả: 341209 (Thay vì 500,000!)
Tại sao? Vì dòng counter += 1 được biên dịch thành 4 bytecode instructions:
LOAD_GLOBAL counterLOAD_CONST 1BINARY_OP +STORE_GLOBAL counter
Nếu thread bị ngắt giữa bước 1 và bước 4, một thread khác sẽ ghi đè lên giá trị cũ. Muốn an toàn, bạn vẫn bắt buộc phải tự dùng threading.Lock().
Phần 4: Giải pháp (Quy tắc chọn công cụ & Kỷ nguyên Free-threading)
1. Bảng phân loại lựa chọn kỹ thuật
Đừng đoán mò. Hãy dùng bảng chẩn đoán sau khi thiết kế hệ thống backend:
| Bài toán | Đặc điểm workload | Công cụ tối ưu | Thư viện đề xuất |
|---|---|---|---|
| High I/O Concurrency | Hàng nghìn kết nối đồng thời, chat realtime, API Gateway, microservices | Asyncio (Event Loop đơn luồng) | asyncio, FastAPI, httpx, aiohttp |
| I/O Đơn giản / Legacy | Tải file, gọi vài chục third-party API trong codebase đồng bộ cũ | Threading (Multi-threading) | concurrent.futures.ThreadPoolExecutor |
| CPU-bound tính toán | Resize ảnh, nén dữ liệu, mã hóa, tính toán thống kê thuần Python | Multiprocessing (Đa tiến trình) | concurrent.futures.ProcessPoolExecutor, celery |
| Tính toán ma trận / ML | Xử lý mảng số học lớn, Deep Learning | C/C++ Extensions (Tự nhả GIL) | numpy, pandas, polars, torch |
2. Tương lai: Free-threaded Python (PEP 703 & Python 3.13+)
Bắt đầu từ Python 3.13, cộng đồng Python đã chính thức giới thiệu bản dựng thử nghiệm Free-threaded CPython (no-GIL) theo đề xuất PEP 703.
Khi chạy ở chế độ no-GIL:
- CPython thay thế Reference Counting toàn cục bằng Mimalloc / Biased Reference Counting.
- Các Thread trong cùng 1 Process giờ đây có thể chạy CPU bytecode song song 100% trên nhiều CPU Core.
Vậy nếu bỏ GIL thì Multiprocessing có chết không?
Không. Multiprocessing vẫn sẽ tồn tại và giữ vai trò tối quan trọng vì một thuộc tính duy nhất: Process Isolation (Sự cô lập tiến trình).
- Chống sập dây chuyền (Crash Containment): Trong mô hình đa luồng, nếu một thread gặp lỗi Segfault (lỗi bộ nhớ C) hoặc deadlock, toàn bộ process sụp đổ. Trong mô hình đa tiến trình, nếu Worker B chết, Worker A và Main Process vẫn sống khỏe re.
- Giới hạn tài nguyên (Resource Sandboxing): Bạn có thể dùng OS cgroups để giới hạn Process A chỉ được dùng tối đa 2GB RAM và 1 Core CPU. Luồng (thread) không làm được việc này một cách độc lập.
- Kiến trúc Web Server chuẩn mực (Gunicorn / Uvicorn): Các web server production luôn chạy mô hình Multi-Process Master-Worker để tận dụng tất cả CPU Cores và tự động khởi động lại worker khi có sự cố.
Mô hình tư duy chốt hạ
3 Nguyên lý vàng cần ghi nhớ:
- Process ≠ Core: 1 Process (cha) chứa
nThread (con) dùng chung một heap, và OS có thể trảinThread đó lênnCore — y như cách nó trảinProcess độc lập lênnCore. Core là phần cứng; Process và Thread là khái niệm của OS được xếp lên phần cứng đó. - GIL chỉ chặn CPU-bound bytecode: Threading trong Python vẫn tăng tốc vượt trội cho I/O-bound vì CPython tự động nhả GIL khi đợi mạng/đĩa.
- Multiprocessing mang lại Isolation: Kể cả khi Python tương lai không còn GIL, đa tiến trình vẫn là vũ khí số một cho sự ổn định và độ tin cậy của hệ thống production.
Bài viết liên quan
Caching & Redis: Mô hình tư duy 'Tờ giấy nhớ'
Tại sao Redis làm mọi thứ nhanh hơn? Hướng dẫn chuyên sâu về cache invalidation (vấn đề khó nhất CS), các chiến lược eviction, và Redis data types.
Khi Nào Nên Dùng Class vs. Function Trong Python: Checklist Thiết Kế
Mental checklist lặp lại được cho Python developer: khi nào dùng class, khi nào dùng function, instance state và dependency injection.
MoneyPrinterV2: 18.000 Sao GitHub Và Cái Máy In Nội Dung Tự Động
Ollama viết kịch bản, KittenTTS đọc, Gemini vẽ hình, MoviePy ghép video. Bạn ngồi uống cà phê. YouTube Short tự lên kênh.
Giải Mã Super Agent Harness: Đi Sâu Vào DeerFlow Của Bytedance
Khám phá cách DeerFlow 2.0 chuyển mình từ một công cụ nghiên cứu chuyên sâu thành một hệ sinh thái agent toàn diện với sandbox, sub-agent và bộ nhớ dài hạn.