Câu 1Vì GIL (Global Interpreter Lock) của Python, tăng số thread không song song hóa được loại công việc nào?
threading và multiprocessing — Thread, process và GIL
Phân biệt thread vs. process, lý do GIL không cho CPU-bound chạy song song, ThreadPoolExecutor và multiprocessing.Pool, subprocess cho lệnh ngoài, và lưu đồ lựa chọn.
Bộ công cụ đồng thời và song song của Python — threading (thread), multiprocessing (process) và subprocess (lệnh ngoài) — được đặt cạnh nhau. Sandbox Python chạy trên trình duyệt không thể tạo thread thật hay process thật, nên bài viết này dùng sơ đồ và các khối code chỉ để đọc để chốt các khái niệm.
Code ở đây không thực sự chạy
Các khối code trong bài viết này là ví dụ dành cho môi trường Python thật. Các lệnh gọi như threading.Thread.start() hay multiprocessing.Pool.map() sẽ không chạy trong sandbox trình duyệt vì runtime không thể tạo thread OS hay process OS. Thay vào đó, có một bài quiz kiểm tra khái niệm ở cuối bài.
Process vs. thread
Một process là đơn vị thực thi mà OS cấp phát — nó có không gian bộ nhớ riêng, và các process không can thiệp lẫn nhau. Một thread là đơn vị thực thi nhẹ bên trong một process, và chia sẻ bộ nhớ với các thread anh em. Việc chia sẻ làm cho truyền dữ liệu nhanh, nhưng bạn phải để ý đến race condition (nhiều thread cùng ghi vào một biến cùng lúc và làm hỏng kết quả).
Mô hình tinh thần đơn giản: "process = nặng nhưng độc lập, thread = nhẹ nhưng chia sẻ".
- Đơn vị thực thi độc lập theo cách OS nhìn
- Bộ nhớ độc lập · Không bị ràng buộc GIL
- Chi phí khởi tạo cao; song song thật hoạt động
- Đơn vị thực sự chạy code bên trong process
- Bộ nhớ chia sẻ · Bị ràng buộc bởi GIL
- Hiệu quả cho công việc I/O-bound
- Chạy bằng cách chuyển lượt bên trong một thread duy nhất
- Còn nhẹ hơn nữa; tốt cho I/O đồng thời số lượng lớn
threading và GIL — ràng buộc đối với thread của Python
Python (CPython) có một cơ chế gọi là GIL (Global Interpreter Lock) áp đặt ràng buộc: "chỉ một thread có thể thực thi bytecode Python tại một thời điểm". Đối với công việc nặng CPU, chạy nó trên nhiều thread đồng thời về cơ bản không nhanh hơn một thread duy nhất.
Mặt khác, trong công việc I/O-bound (nơi thời gian bị chi phối bởi việc chờ phản hồi từ bên ngoài — mạng, file, DB) Python giải phóng GIL, nên threading thực sự tăng tốc công việc I/O-bound. Tuy nhiên, nếu workload là I/O-bound, asyncio thường có overhead nhỏ hơn và dễ viết hơn, nên với code mới hãy ưu tiên asyncio.
# threading: API mức thấp
import threading
def worker(name):
print(f"{name} started")
# làm gì đó
print(f"{name} done")
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
t1.start()
t2.start()
t1.join() # chờ hoàn thành
t2.join()
# concurrent.futures: API mức cao (khuyến nghị)
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
# trong code thật: requests.get(url) hoặc công việc I/O-bound khác
return f"fetched: {url}"
with ThreadPoolExecutor(max_workers=4) as executor:
urls = ["a.com", "b.com", "c.com"]
results = list(executor.map(fetch, urls))
print(results)
Với code mới, hãy chọn ThreadPoolExecutor
Dùng threading.Thread trực tiếp khiến việc quản lý vòng đời rối rắm. concurrent.futures.ThreadPoolExecutor an toàn để quản lý với with và cho phép bạn xử lý cả danh sách bằng executor.map(func, iterable) như một API duy nhất. Giới hạn số thread (max_workers) cũng tiện để không làm ngập server bằng quá nhiều kết nối.
Khi nào threading là lựa chọn tốt
threading / ThreadPoolExecutor phát huy khi bạn muốn lấp thời gian chờ bằng việc khác:
- Xử lý đồng thời nhiều Web API / truy vấn DB / file I/O
- Song song hóa các thư viện đồng bộ có sẵn (không hỗ trợ async)
- Đọc đồng thời output từ nhiều process được tạo ra bởi subprocess
- Chạy công việc nền mà không chặn main loop của GUI
multiprocessing — song song thật
multiprocessing là module để tạo nhiều process Python và chạy chúng song song. Vì process không bị ràng buộc GIL, bạn có thể chạy công việc CPU-bound theo kiểu song song thật — trên CPU 4 lõi, xử lý ảnh hoặc số học nặng nhanh lên khoảng 4 lần.
from multiprocessing import Pool
def heavy(n):
return sum(i * i for i in range(n)) # công việc CPU-bound
if __name__ == "__main__": # dạng bắt buộc cho multiprocessing
with Pool(processes=4) as pool:
results = pool.map(heavy, [10**6, 10**6, 10**6, 10**6])
print("sum:", sum(results))
multiprocessing yêu cầu `if __name__ == '__main__':`
multiprocessing hoạt động bằng cách chạy lại script cha trong từng process con, nên gọi Pool(...).map(...) ở top level sẽ kích hoạt đệ quy vô tận và sập. Chế độ khởi động spawn của Windows / macOS đặc biệt nghiêm về điều này — luôn đặt code chính bên trong khối if __name__ == "__main__":.
subprocess — lệnh ngoài
subprocess là module để gọi lệnh ngoài (lệnh shell của OS) từ Python — chạy git status, chuyển đổi video bằng ffmpeg, gọi shell script và các trường hợp "khởi động một chương trình không phải Python" khác. Tên có vẻ giống multiprocessing, nhưng đó là một công cụ hoàn toàn khác.
import subprocess
result = subprocess.run(
["git", "status", "--short"],
capture_output=True,
text=True,
check=True, # sinh CalledProcessError nếu thất bại
)
print(result.stdout)
print("return code:", result.returncode)
Luồng quyết định — chọn cái nào?
Việc chọn giữa asyncio / threading / multiprocessing / subprocess quy về hai trục: "CPU-bound hay I/O-bound?" và "Bên trong Python hay là lệnh ngoài?". Sơ đồ luồng dưới đây loại bỏ phần lớn việc đoán mò.
| Workload | Chọn | Lý do |
|---|---|---|
| Gọi 100 Web API đồng thời | asyncio | I/O-bound; nhẹ và dễ viết |
| Song song hóa client HTTP đồng bộ có sẵn | threading (ThreadPoolExecutor) | Nếu thư viện không hỗ trợ async, dùng threading |
| Song song hóa xử lý ảnh trên 4 lõi | multiprocessing | CPU-bound cần process để tránh GIL |
Chạy lệnh như git hoặc ffmpeg | subprocess | Chuyên dụng để gọi chương trình ngoài Python |
| Hàng triệu phép toán đơn giản | NumPy / Cython | Vector hóa thắng song song hóa ở mức Python |
CPU-bound thực sự hiếm hơn bạn nghĩ
Phần lớn code Python trông như đang kẹt ở tính toán thực ra nhanh lên 100 lần nhờ vector hóa với NumPy / Pandas / Cython. Trước khi đuổi theo gấp 4 lần với multiprocessing, kiểm tra xem nên làm gì trước: NumPy cho số học, Pandas cho dữ liệu, regex được tối ưu cho chuỗi.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Bạn dùng cái nào để gọi các lệnh ngoài như git status từ Python?
Câu 3Cái nào phù hợp nhất để song song hóa thật sự tính toán nặng trên 4 lõi CPU?