0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Ảnh hưởng của GIL đến hiệu suất Python trong thực tế

Đăng vào 2 tuần trước

• 4 phút đọc

Ảnh hưởng của GIL đến hiệu suất Python trong thực tế

Nếu bạn đã từng cố gắng tăng tốc ứng dụng Python của mình bằng cách thêm các thread nhưng không thấy sự thay đổi nào, chào mừng bạn đến với thế giới của Global Interpreter Lock (GIL). Trong bài viết này, chúng ta sẽ phân tích cách GIL ảnh hưởng đến hiệu suất đồng thời, đặc biệt trong những khối công việc backend thực tế.

GIL là gì?

Global Interpreter Lock (GIL) là một mutex ngăn cản nhiều thread native thực thi bytecode Python cùng một lúc trong trình thông dịch CPython. Nó giống như một bảo vệ cửa câu lạc bộ: chỉ một thread có thể vào sân nhảy bytecode Python tại một thời điểm, ngay cả khi có không gian cho nhiều hơn. Điều này có nghĩa là:

  • Các thread trong Python không thực sự đồng thời khi thực hiện công việc CPU.
  • Bạn không nhận được lợi ích hiệu suất miễn phí chỉ bằng cách tạo ra các thread cho tính toán nặng.
  • Mã nặng về I/O vẫn có thể hưởng lợi từ threading, vì GIL được giải phóng trong quá trình I/O.

Công việc CPU-Bound và I/O-Bound: Tại sao lại quan trọng?

Đầu tiên, hãy định nghĩa chúng:

  • CPU-bound: Mã chủ yếu sử dụng chu kỳ CPU như xử lý dữ liệu, xử lý hình ảnh.
  • I/O-bound: Mã chủ yếu chờ đợi như các yêu cầu mạng, gọi cơ sở dữ liệu, I/O tệp.

Tại sao điều này lại quan trọng? Bởi vì GIL gây bất lợi cho bạn nhiều nhất khi mã của bạn là CPU-bound. Đó là lúc các thread tranh giành GIL. Nhưng đối với mã I/O-bound, các thread lần lượt thực hiện tốt, vì GIL được giải phóng trong quá trình I/O.

Ví dụ #1: Nhiệm vụ CPU-Bound (Sử dụng Threads)

Hãy mô phỏng một khối công việc nặng CPU bằng cách sử dụng các thread:

python Copy
import threading
import time

COUNT = 50_000_000

def cpu_heavy():
    x = 0
    for _ in range(COUNT):
        x += 1

start = time.time()

thread1 = threading.Thread(target=cpu_heavy)
thread2 = threading.Thread(target=cpu_heavy)

thread1.start()
thread2.start()
thread1.join()
thread2.join()

print(f"Threads (CPU-bound): {time.time() - start:.2f}s")

Điều bạn sẽ thấy: Hai thread không làm cho quá trình nhanh hơn. Nó có thể thậm chí còn lâu hơn khi chạy một cái sau cái khác.

Tại sao? Bởi vì chúng đều đang tranh giành GIL. Chỉ một thread có thể thực hiện công việc Python thực tế tại một thời điểm.

Ví dụ #2: Nhiệm vụ CPU-Bound (Sử dụng Multiprocessing)

Bây giờ hãy thử khối công việc tương tự bằng cách sử dụng multiprocessing:

python Copy
from multiprocessing import Process
import time

COUNT = 50_000_000

def cpu_heavy():
    x = 0
    for _ in range(COUNT):
        x += 1

start = time.time()

p1 = Process(target=cpu_heavy)
p2 = Process(target=cpu_heavy)

p1.start()
p2.start()
p1.join()
p2.join()

print(f"Processes (CPU-bound): {time.time() - start:.2f}s")

Bây giờ thì nhanh hơn nhiều! Mỗi process có GIL và không gian bộ nhớ riêng — vì vậy chúng có thể chạy song song trên các lõi CPU.

Ví dụ #3: Nhiệm vụ I/O-Bound (Sử dụng Threads)

Hãy mô phỏng một khối công việc backend thực tế — gọi một API chậm:

python Copy
import threading
import time
import requests

URL = "https://httpbin.org/delay/2"  # chờ 2 giây trước khi trả lời

def fetch():
    response = requests.get(URL)
    print(response.status_code)

start = time.time()

threads = [threading.Thread(target=fetch) for _ in range(5)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Threads (I/O-bound): {time.time() - start:.2f}s")

Dù mỗi yêu cầu mất 2 giây, toàn bộ chương trình hoàn thành trong khoảng 2 giây, không phải 10.

Tại sao? Bởi vì requests.get() chặn trên I/O và giải phóng GIL, cho phép các thread khác chạy.

Threads hoạt động tốt với I/O

Trong các API backend, bạn thường:

  • Gọi các API khác
  • Nói chuyện với các cơ sở dữ liệu
  • Đọc các tệp

Tất cả đều là I/O-bound, vì vậy threading thực sự có thể giúp ngay cả với GIL. Ví dụ, một endpoint FastAPI đơn giản như:

python Copy
@app.get("/status")
def get_status():
    requests.get("https://a-service.com/ping")
    return {"status": "ok"}

Có thể mở rộng tốt hơn dưới người dùng đồng thời nếu được phục vụ bởi một máy chủ ASGI dựa trên thread như Uvicorn với workers.

So sánh: asyncio vs multiprocessing vs threading

Loại Nhiệm Vụ Công Cụ Tốt Nhất Tại Sao
CPU-bound multiprocessing Tránh GIL bằng cách sử dụng processes
I/O-bound threading / asyncio GIL được giải phóng trong I/O
Hỗn Hợp Chia nhỏ công việc Sử dụng thread cho I/O, process cho CPU

Tổng kết

  • Các thread Python sẽ không giúp bạn tăng tốc mã nặng CPU vì GIL.
  • Đối với công việc CPU-bound, hãy sử dụng multiprocessing, hoặc gọi đến các mở rộng C giải phóng GIL.
  • Đối với công việc nặng I/O, các thread Python (hoặc asyncio) rất tốt.
  • Hãy benchmark mã của bạn trước khi tối ưu hóa. Bạn có thể đang đổ lỗi cho điều sai lầm.

Bạn có thể tham khảo bài viết gốc tại đây.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào