---
title: "Python Concurrency: Hiểu GIL, Threading và Multiprocessing từ gốc"
description: "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."
pubDate: "2026-08-16"
lang: "vi"
slug: "python-gil"
url: "https://hoangyell.com/vi/python-gil/"
author: "HoangYell"
translation_en: "https://hoangyell.com/python-gil/"
categories: ["System Design"]
tags: ["python", "concurrency", "backend", "performance", "architecture"]
heroImage: "https://raw.githubusercontent.com/HoangGeek/store/refs/heads/main/webp/python-gil.webp"
---

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 `n` Thread 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.

```markdy
interactive true
scene width=1100 height=520
theme: light=doodle dark=nebula
layout TB

process PythonProc "1 Python Process (Cha)"
worker T1 "Thread 1 (Con)"
worker T2 "Thread 2 (Con)"
worker T3 "Thread 3 (Con)"
node Core1 "CPU Core 1" focal=true
node Core2 "CPU Core 2"
node Core3 "CPU Core 3"

beat spawn "Bước 1 - Process cha sinh ra n Thread con" {
  show $nodes stagger=40ms
  PythonProc -> T1 "spawn"
  PythonProc -> T2 "spawn"
  PythonProc -> T3 "spawn"
  glow PythonProc color=#38bdf8
}

beat dispatch "Bước 2 - n Thread phân phối lên n CPU Core" {
  T1 -> Core1 "thực thi"
  T2 -> Core2 "thực thi"
  T3 -> Core3 "thực thi"
  glow Core1 color=#22c55e & glow Core2 color=#22c55e & glow Core3 color=#22c55e
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

> **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.

```markdy
interactive true
scene width=1100 height=440
theme: light=doodle dark=nebula
layout TB

process Proc1 "Process 1 (Cha)"
process Proc2 "Process 2 (Cha)"
process Proc3 "Process 3 (Cha)"
node Core1 "CPU Core 1" focal=true
node Core2 "CPU Core 2"
node Core3 "CPU Core 3"

beat parallel "n Process chạy song song thực sự trên n CPU Core" {
  show $nodes stagger=40ms
  Proc1 -> Core1 "thực thi độc lập"
  Proc2 -> Core2 "thực thi độc lập"
  Proc3 -> Core3 "thực thi độc lập"
  glow Proc1 color=#38bdf8 & glow Proc2 color=#38bdf8 & glow Proc3 color=#38bdf8
  glow Core1 color=#22c55e & glow Core2 color=#22c55e & glow Core3 color=#22c55e
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

### 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.

```markdy
interactive true
scene width=1100 height=600
theme: light=doodle dark=nebula
layout auto

worker ThreadA "Luồng A (Thread A)"
worker ThreadB "Luồng B (Thread B)"
lock GIL "Khóa GIL (1 Micro Duy Nhất)" focal=true
node CPUCore "Lõi CPU Vật lý (CPU Core)"

beat thread_a "1. Luồng A giữ GIL để chạy Bytecode" {
  show $nodes stagger=50ms
  frame ThreadA GIL CPUCore zoom=1.12
  ThreadA -> GIL "nắm giữ GIL mutex"
  GIL -> CPUCore "thực thi trên CPU Core"
  glow GIL color=#ef4444
}

beat thread_b_wait "2. Luồng B bị chặn đợi đến lượt" {
  ThreadB -> GIL "bị chặn (chờ GIL)"
  glow ThreadB color=#f59e0b
}

beat yield_switch "3. Luồng A nhả GIL -> Luồng B thực thi" {
  frame ThreadB GIL CPUCore zoom=1.12
  ThreadA ~> GIL "nhả khóa khi hết tick/I-O"
  ThreadB -> GIL "nhận khóa GIL"
  GIL -> CPUCore "thực thi tiếp trên CPU Core"
  glow CPUCore color=#22c55e
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

### 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:
1.  **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).
2.  **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).
3.  **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.**

```markdy
interactive true
scene type=sequence width=1100 height=600
theme: light=doodle dark=nebula

participant Thread1 "Thread 1 (Gọi API)"
participant GIL "GIL Mutex"
participant OS "Socket HĐH"
participant Server "Remote API"

beat req1 "1. Gửi Request & Tự Nguyện Nhả GIL" {
  show $nodes stagger=50ms
  Thread1 -> GIL "giữ GIL"
  Thread1 -> OS "ghi socket data"
  OS -> Server "HTTP GET /items"
  Thread1 ~> GIL "tự nguyện nhả GIL (chờ I/O)"
  glow GIL color=#22c55e
}

beat req2 "2. Nhận Response & Tái Chiếm GIL" {
  OS <- Server "HTTP 200 OK"
  OS -> Thread1 "dữ liệu đã về"
  Thread1 -> GIL "lấy lại GIL"
  Thread1 -> Thread1 "parse JSON"
  glow Thread1 color=#38bdf8
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

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:

```python
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`:

1.  **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).
2.  **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:

```python
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:
1. `LOAD_GLOBAL counter`
2. `LOAD_CONST 1`
3. `BINARY_OP +`
4. `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)

```markdy
interactive true
scene width=1150 height=600
theme: light=doodle dark=nebula
layout auto

cluster MasterProc "Tiến trình Gốc (Master)"
cluster WorkerA "Worker A (RAM Riêng)"
cluster WorkerB "Worker B (RAM Riêng)"
queue IPC "Hàng đợi IPC (Pickle Queue)" focal=true
node Cores "Đa Lõi CPU (Parallel Cores)"

beat dispatch "1. Master gửi việc qua IPC Queue" {
  show $nodes stagger=50ms
  MasterProc -> IPC "pickle serialize task"
  glow IPC color=#f59e0b
}

beat compute "2. Các Worker chạy song song 100% trên Đa Core" {
  WorkerA -> IPC "nhận task" & WorkerB -> IPC "nhận task"
  WorkerA -> Cores "tính toán Core 1"
  WorkerB -> Cores "tính toán Core 2"
  glow Cores color=#22c55e
}

beat fault_isolation "3. Worker B lỗi sập không ảnh hưởng Master" {
  glow WorkerB color=#ef4444
  MasterProc -> WorkerA "Master & Worker A vẫn chạy bình thường"
  glow MasterProc color=#22c55e
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

### 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ạ

```markdy
interactive true
scene type=flowchart width=1150 height=650
theme: light=doodle dark=nebula
layout TB

decision Workload "Workload Concurrency" focal=true
condition IOBound "I/O-Bound (Chờ Mạng, DB, Đĩa)"
condition CPUBound "CPU-Bound (Tính toán nặng)"
service AsyncioTool "Asyncio / FastAPI (Event Loop tải cao)"
service MultiProcTool "Multiprocessing (Tận dụng Đa Core)"

edge e1: Workload -> IOBound "chờ I/O"
edge e2: Workload -> CPUBound "CPU 100%"
edge e3: IOBound -> AsyncioTool "hàng nghìn kết nối"
edge e4: CPUBound -> MultiProcTool "chạy đa tiến trình"

beat main_flow "Quy trình Ra Quyết định" {
  show $nodes stagger=50ms
  Workload -> IOBound
  Workload -> CPUBound
  IOBound -> AsyncioTool
  CPUBound -> MultiProcTool
  glow AsyncioTool color=#22c55e & glow MultiProcTool color=#22c55e
}

player:
    playback:
      loop true
    controls:
      speed true
      speeds "0.25"
      fullscreen true
      gif true
      theme true
    interaction:
      zoom true
      pan true
      click_to_play true
      double_click_to_reset true
      keyboard false
    chrome:
      badge true
      progress none
```

### 3 Nguyên lý vàng cần ghi nhớ:

1.  **Process ≠ Core**: 1 Process (cha) chứa `n` Thread (con) dùng chung một heap, và OS có thể trải `n` Thread đó lên `n` Core - y như cách nó trải `n` Process độc lập lên `n` Core. 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 đó.
2.  **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.
3.  **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.
