Skip to content

GIL & Threading Deep Tech

GIL = Kẻ thù số 1 của Python Performance

GIL là gì?

GIL (Global Interpreter Lock) là một mutex (khóa) trong CPython interpreter, chỉ cho phép một thread thực thi Python bytecode tại một thời điểm, bất kể bạn có bao nhiêu CPU cores.

Tại sao Python có GIL?

  1. Memory management đơn giản: CPython dùng reference counting, GIL bảo vệ reference count khỏi race conditions
  2. Tích hợp C extensions dễ dàng: Không cần lo thread-safety trong C code
  3. Lịch sử: Python ra đời năm 1991, khi single-core là chuẩn

Ảnh hưởng của GIL

CPU-bound Tasks: GIL là Thảm họa

python
import time
import threading

def tinh_toan_nang():
    """CPU-bound: tính toán nhiều."""
    tong = 0
    for i in range(50_000_000):
        tong += i
    return tong

# Single-threaded
bat_dau = time.perf_counter()
tinh_toan_nang()
tinh_toan_nang()
print(f"Single-threaded: {time.perf_counter() - bat_dau:.2f}s")

# Multi-threaded (hy vọng nhanh hơn?)
bat_dau = time.perf_counter()
t1 = threading.Thread(target=tinh_toan_nang)
t2 = threading.Thread(target=tinh_toan_nang)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Multi-threaded: {time.perf_counter() - bat_dau:.2f}s")

# Kết quả đáng buồn:
# Single-threaded: 4.2s
# Multi-threaded: 4.5s  ← CHẬM HƠN vì context switching!

I/O-bound Tasks: GIL Không ảnh hưởng nhiều

python
import time
import threading
import requests

def goi_api():
    """I/O-bound: chờ network."""
    response = requests.get("https://api.github.com")
    return response.status_code

# Single-threaded: tuần tự
bat_dau = time.perf_counter()
for _ in range(5):
    goi_api()
print(f"Single-threaded: {time.perf_counter() - bat_dau:.2f}s")

# Multi-threaded: đồng thời (GIL được release khi chờ I/O)
bat_dau = time.perf_counter()
threads = [threading.Thread(target=goi_api) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Multi-threaded: {time.perf_counter() - bat_dau:.2f}s")

# Kết quả:
# Single-threaded: 2.5s
# Multi-threaded: 0.6s  ← 4x nhanh hơn!
)

💡 QUY TẮC QUAN TRỌNG

  • CPU-bound: Threading KHÔNG giúp ích (GIL block)
  • I/O-bound: Threading có hiệu quả (GIL được release khi chờ I/O)

Giải pháp cho CPU-bound: multiprocessing

multiprocessing tạo các process riêng biệt, mỗi process có GIL riêng.

python
import time
from multiprocessing import Process, Pool

def tinh_toan_nang():
    tong = 0
    for i in range(50_000_000):
        tong += i
    return tong

# Multiprocessing - thực sự song song!
if __name__ == "__main__":
    bat_dau = time.perf_counter()
    
    p1 = Process(target=tinh_toan_nang)
    p2 = Process(target=tinh_toan_nang)
    
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    
    print(f"Multiprocessing: {time.perf_counter() - bat_dau:.2f}s")
    # Kết quả: 2.1s (gần gấp đôi nhanh hơn!)

Pool cho nhiều tasks

python
from multiprocessing import Pool

def binh_phuong(n):
    return n ** 2

if __name__ == "__main__":
    with Pool(processes=4) as pool:
        ket_qua = pool.map(binh_phuong, range(1000000))

So sánh Threading vs Multiprocessing

Đặc điểmThreadingMultiprocessing
GILBị ảnh hưởngKhông ảnh hưởng
CPU-bound❌ Không hiệu quả✅ Hiệu quả
I/O-bound✅ Hiệu quả✅ Hiệu quả (overkill)
MemoryChia sẻRiêng biệt (tốn hơn)
OverheadThấpCao
CommunicationDễ (shared memory)Phức tạp (IPC)

Giải pháp cho I/O-bound: asyncio

Cho I/O-bound tasks, asyncio là lựa chọn tốt nhất (xem bài tiếp theo).

import asyncio import aiohttp

async def goi_api_async(session, url): async with session.get(url) as response: return await response.json()

async def main(): async with aiohttp.ClientSession() as session: tasks = [goi_api_async(session, f"https://api.example.com/{i}") for i in range(100)] ket_qua = await asyncio.gather(*tasks) return ket_qua

Gọi 100 APIs gần như cùng lúc!

asyncio.run(main()) )] asyncio.run(main())


---

## Bảng Quyết định

```mermaid
flowchart TD
    A[Task của bạn là gì?] --> B{CPU-intensive?}
    B -->|Có| C[multiprocessing]
    B -->|Không| D{I/O-intensive?}
    D -->|Có| E{Cần scale lớn?}
    E -->|Có| F[asyncio]
    E -->|Không| G[threading]
    D -->|Không| H[Không cần concurrency]
Loại TaskVí dụGiải pháp
CPU-boundXử lý hình ảnh, ML training, nén filemultiprocessing
I/O-bound (ít)Đọc vài files, vài API callsthreading
I/O-bound (nhiều)Web scraping, API gateway, chat serverasyncio
CPU + I/OVideo encoding + uploadmultiprocessing + asyncio

Tại sao Penrift dùng Golang thay vì Python?

⚠️ ARCHITECTURE INSIGHT

Khi xây dựng Penrift — một hệ thống tunnel hiệu năng cao — chúng tôi đã chọn Golang thay vì Python vì:

  1. GIL: Python không thể xử lý hàng nghìn kết nối đồng thời hiệu quả
  2. Goroutines: Golang có native concurrency không bị giới hạn như GIL
  3. Performance: Golang compiled code nhanh hơn Python 10-100x cho network operations
  4. Memory: Golang sử dụng ít RAM hơn cho mỗi connection

Đọc thêm về kiến trúc Penrift trong tài liệu kỹ thuật của chúng tôi.


Tương lai: Removing the GIL

Python 3.12: GIL Improvements

Python 3.12 đã có một số cải tiến quan trọng cho GIL:

python
# Python 3.12+ - Per-interpreter GIL (PEP 684)
# Mỗi sub-interpreter có GIL riêng!

import _xxsubinterpreters as interpreters

# Tạo sub-interpreter với GIL riêng
interp_id = interpreters.create()

# Chạy code trong sub-interpreter
interpreters.run_string(interp_id, """
import time
# Code này chạy với GIL riêng!
result = sum(range(10_000_000))
print(f"Result: {result}")
""")

interpreters.destroy(interp_id)

💡 Per-interpreter GIL

  • Mỗi sub-interpreter có GIL riêng
  • Cho phép true parallelism giữa các interpreters
  • Hạn chế: Không share objects trực tiếp giữa interpreters
  • Use case: Isolated workloads, plugin systems

Python 3.13: Free-threaded Python (Experimental)

PEP 703 - Making the GIL Optional! Python 3.13 giới thiệu experimental build không có GIL.

bash
# Build Python 3.13+ với --disable-gil
./configure --disable-gil
make -j4

# Hoặc dùng pre-built binary (khi available)
# python3.13t (t = free-threaded)
python
# Kiểm tra GIL status
import sys

# Python 3.13+
if hasattr(sys, '_is_gil_enabled'):
    print(f"GIL enabled: {sys._is_gil_enabled()}")
else:
    print("GIL status check not available")

# Với free-threaded Python, threading thực sự parallel!
import threading
import time

def cpu_work():
    total = 0
    for i in range(50_000_000):
        total += i
    return total

# Với free-threaded Python: ~2x speedup trên 2 cores!
start = time.perf_counter()
t1 = threading.Thread(target=cpu_work)
t2 = threading.Thread(target=cpu_work)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Time: {time.perf_counter() - start:.2f}s")

Free-threaded Python: Caveats

⚠️ EXPERIMENTAL - CHƯA PRODUCTION READY

Những điều cần biết:

  1. Performance overhead: Single-threaded code có thể chậm hơn 5-10%
  2. C Extension compatibility: Nhiều C extensions chưa thread-safe
  3. Memory usage: Tăng do fine-grained locking
  4. Ecosystem: NumPy, Pandas, etc. cần update để hỗ trợ

Timeline dự kiến:

  • Python 3.13 (2024): Experimental, opt-in
  • Python 3.14 (2025): Stabilization
  • Python 3.15+ (2026+): Possible default
python
# Kiểm tra C extension có thread-safe không
import numpy as np

# Python 3.13+ với free-threading
if hasattr(np, '__numpy_threading_mode__'):
    print(f"NumPy threading mode: {np.__numpy_threading_mode__}")

Khi nào Threading giúp ích vs gây hại?

Threading GIÚP ÍCH khi:

I/O-bound tasks - GIL được release khi chờ I/O

import threading import requests

def fetch_url(url): response = requests.get(url) # GIL released during network wait return response.status_code

10 threads = ~10x speedup cho I/O-bound

urls = ["https://api.github.com"] * 10 threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls] for t in threads: t.start() for t in threads: t.join() ] for t in threads: t.join()


```python
# ✅ Blocking operations - file I/O, database queries
import threading
import sqlite3

def query_db(query):
    conn = sqlite3.connect('data.db')  # GIL released during disk I/O
    cursor = conn.execute(query)
    return cursor.fetchall()

# Multiple queries đồng thời
queries = ["SELECT * FROM users", "SELECT * FROM orders", "SELECT * FROM products"]
threads = [threading.Thread(target=query_db, args=(q,)) for q in queries]
python
# ✅ GUI applications - Keep UI responsive
import threading
import tkinter as tk

def long_task():
    # Chạy trong background thread
    result = expensive_computation()
    # Update UI từ main thread
    root.after(0, lambda: label.config(text=result))

root = tk.Tk()
label = tk.Label(root, text="Processing...")
button = tk.Button(root, text="Start", 
                   command=lambda: threading.Thread(target=long_task).start())

Threading GÂY HẠI khi:

python
# ❌ CPU-bound tasks - GIL blocks parallel execution
import threading
import time

def cpu_intensive():
    total = 0
    for i in range(50_000_000):
        total += i  # Pure Python - GIL held!
    return total

# 2 threads KHÔNG nhanh hơn 1 thread!
start = time.perf_counter()
t1 = threading.Thread(target=cpu_intensive)
t2 = threading.Thread(target=cpu_intensive)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"2 threads: {time.perf_counter() - start:.2f}s")  # ~4.5s

# Single thread
start = time.perf_counter()
cpu_intensive()
cpu_intensive()
print(f"Sequential: {time.perf_counter() - start:.2f}s")  # ~4.2s (NHANH HƠN!)
python
# ❌ Shared mutable state - Race conditions
import threading

counter = 0

def increment():
    global counter
    for _ in range(1_000_000):
        counter += 1  # NOT atomic! Race condition!

threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Expected: 4,000,000, Got: {counter}")  # Thường < 4,000,000!
python
# ✅ FIX: Dùng Lock
import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(1_000_000):
        with lock:  # Thread-safe
            counter += 1

# Nhưng lock làm chậm đáng kể!

Bảng quyết định: Threading vs Alternatives

ScenarioThreadingMultiprocessingAsyncio
I/O-bound, few tasks✅ Best⚠️ Overkill✅ Good
I/O-bound, many tasks⚠️ OK⚠️ Overkill✅ Best
CPU-bound❌ Bad✅ Best❌ Bad
GUI responsiveness✅ Best❌ Complex⚠️ OK
Shared state needed⚠️ Careful❌ Complex✅ Easy
Memory efficiency✅ Good❌ High overhead✅ Best

Production Pitfalls

⚠️ NHỮNG LỖI THƯỜNG GẶP VỚI THREADING

1. Race Conditions - Kẻ giết người thầm lặng

python
# ❌ SAI - Race condition
class BankAccount:
    def __init__(self):
        self.balance = 1000
    
    def withdraw(self, amount):
        if self.balance >= amount:  # Check
            time.sleep(0.001)  # Simulate processing
            self.balance -= amount  # Modify - RACE!
            return True
        return False

# 2 threads cùng withdraw 800 từ balance 1000
# Cả 2 đều thấy balance >= 800, cả 2 đều withdraw!
# Kết quả: balance = -600 😱
python
# ✅ ĐÚNG - Dùng Lock
import threading

class SafeBankAccount:
    def __init__(self):
        self.balance = 1000
        self._lock = threading.Lock()
    
    def withdraw(self, amount):
        with self._lock:  # Atomic operation
            if self.balance >= amount:
                time.sleep(0.001)
                self.balance -= amount
                return True
            return False

2. Deadlocks - Threads chờ nhau mãi mãi

python
# ❌ SAI - Deadlock
lock_a = threading.Lock()
lock_b = threading.Lock()

def thread_1():
    with lock_a:
        time.sleep(0.1)
        with lock_b:  # Chờ lock_b
            print("Thread 1 done")

def thread_2():
    with lock_b:
        time.sleep(0.1)
        with lock_a:  # Chờ lock_a - DEADLOCK!
            print("Thread 2 done")
python
# ✅ ĐÚNG - Consistent lock ordering
def thread_1():
    with lock_a:  # Luôn acquire lock_a trước
        with lock_b:
            print("Thread 1 done")

def thread_2():
    with lock_a:  # Cùng thứ tự!
        with lock_b:
            print("Thread 2 done")

3. Thread Leaks - Quên join threads

python
# ❌ SAI - Thread leak
def process_request(data):
    thread = threading.Thread(target=heavy_work, args=(data,))
    thread.start()
    # Quên join! Thread có thể chạy mãi

# ✅ ĐÚNG - Dùng ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=10) as executor:
    futures = [executor.submit(heavy_work, data) for data in items]
    results = [f.result() for f in futures]
# Tự động cleanup khi exit context

4. GIL Misconception - Nghĩ threading = parallel

python
# ❌ SAI - Expect speedup cho CPU-bound
def matrix_multiply(a, b):
    # Pure Python matrix multiplication
    # GIL held entire time!
    pass

# Threading KHÔNG giúp ích!
# Dùng multiprocessing hoặc NumPy thay thế

Deep Dive vào Python Internals

💡 NÂNG CAO KIẾN THỨC

Muốn hiểu sâu hơn về GIL, CPython internals, và optimization techniques? Chúng tôi đã tổng hợp tất cả trong ebook chuyên sâu.

👑PREMIUM

HPN Interview Kit

Bộ câu hỏi phỏng vấn Big Tech

  • ✓ 200+ câu hỏi thực tế từ FAANG
  • ✓ Giải thích chi tiết bằng Tiếng Việt
  • ✓ Cập nhật liên tục 2025
Sở hữu ngay99k

Bảng Tóm tắt

python
# === THREADING (I/O-bound) ===
import threading

def worker():
    # I/O operations - GIL released
    pass

threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# === MULTIPROCESSING (CPU-bound) ===
from multiprocessing import Process, Pool

def cpu_worker(data):
    return heavy_computation(data)

if __name__ == "__main__":
    # Pool cho nhiều tasks
    with Pool(4) as pool:
        results = pool.map(cpu_worker, data_list)
    
    # Process cho tasks riêng lẻ
    p = Process(target=cpu_worker, args=(data,))
    p.start()
    p.join()

# === CONCURRENT.FUTURES (High-level API) ===
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# Thread pool cho I/O
with ThreadPoolExecutor(max_workers=10) as executor:
    results = executor.map(io_worker, urls)

# Process pool cho CPU
with ProcessPoolExecutor(max_workers=4) as executor:
    results = executor.map(cpu_worker, data)

# === THREAD SAFETY ===
import threading

# Lock cho shared state
lock = threading.Lock()
with lock:
    shared_resource += 1

# RLock cho recursive locking
rlock = threading.RLock()

# Semaphore cho limited concurrency
sem = threading.Semaphore(5)
with sem:
    access_limited_resource()

# Event cho signaling
event = threading.Event()
event.set()    # Signal
event.wait()   # Wait for signal
event.clear()  # Reset

# === PYTHON 3.12+ ===
# Per-interpreter GIL
import _xxsubinterpreters as interpreters
interp_id = interpreters.create()
interpreters.run_string(interp_id, "print('Hello')")

# === PYTHON 3.13+ (EXPERIMENTAL) ===
# Free-threaded Python - no GIL!
# Build with: ./configure --disable-gil
import sys
if hasattr(sys, '_is_gil_enabled'):
    print(f"GIL: {sys._is_gil_enabled()}")

# === DECISION GUIDE ===
# I/O-bound (few tasks)    → threading
# I/O-bound (many tasks)   → asyncio
# CPU-bound                → multiprocessing
# GUI responsiveness       → threading
# Need shared state        → threading + locks