Python Performance: Khi nào nên dùng Multiprocessing, khi nào chọn Threading?

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Nỗi ức chế: Máy 16 core nhưng Python chỉ chạy 1 core?

Bạn vừa sắm một dàn workstation cực khủng với CPU 16 nhân, 32 luồng. Bạn hí hửng chạy script xử lý dữ liệu nhưng Task Manager lại báo tin buồn: Chỉ có đúng một nhân CPU đang gánh team, còn lại đều đang “ngồi chơi xơi nước”. Tại sao vậy?

Thủ phạm chính là Global Interpreter Lock (GIL) của CPython. Cơ chế này ngăn cản các luồng (threads) chạy mã Python cùng một lúc trên đa nhân. Để thoát khỏi cảnh nghẽn cổ chai này, bạn cần hiểu rõ sự khác biệt giữa Threading và Multiprocessing. Chọn sai công cụ không chỉ làm code chậm đi mà còn gây lãng phí tài nguyên hệ thống.

Hình ảnh hóa: Nhà hàng và các đầu bếp

Hãy tưởng tượng bạn đang vận hành một bếp ăn công nghiệp:

  • Threading (Đa luồng): Một đầu bếp có 4 tay. Anh ta có thể vừa lật thịt, vừa canh nồi súp. Nhưng vì chỉ có một bộ não, anh ta phải luân phiên chú ý từng việc. Nếu phải giải một bài toán phức tạp, anh ta vẫn phải dừng tay để suy nghĩ.
  • Multiprocessing (Đa tiến trình): Bạn thuê hẳn 4 đầu bếp đứng ở 4 gian bếp độc lập. Mỗi người một thớt, một dao, một bếp riêng. Họ làm việc song song hoàn toàn. Nếu một người chẳng may bị bỏng (crash), 3 người còn lại vẫn ra món bình thường.

1. Threading: Cứu cánh cho các tác vụ chờ đợi

Trong Python, Threading không giúp bạn tính toán nhanh hơn. Nó sinh ra để giải quyết bài toán I/O-bound. Đây là những tác vụ mà CPU dành phần lớn thời gian để… chờ. Chờ dữ liệu từ ổ cứng, chờ phản hồi từ API hoặc chờ kết quả từ Database.

Khi Thread 1 đang đợi phản hồi từ server, GIL sẽ được giải phóng để Thread 2 nhảy vào làm việc. Nhờ đó, tổng thời gian thực thi giảm xuống đáng kể dù CPU không cần gồng mình tính toán.

2. Multiprocessing: Sức mạnh cơ bắp thực thụ

Muốn tận dụng tối đa CPU 16 nhân? Hãy dùng Multiprocessing. Mỗi process sẽ sở hữu một Python interpreter và vùng nhớ riêng biệt. Cách này giúp bạn hoàn toàn né được cái bóng của GIL. Đây là lựa chọn số 1 cho các tác vụ CPU-bound như xử lý ảnh, encode video hoặc tính toán ma trận lớn.

Thực chiến: Con số không biết nói dối

Hãy cùng so sánh hiệu quả qua hai kịch bản phổ biến nhất.

Kịch bản 1: Cào dữ liệu từ 100 website (I/O-bound)

Nếu chạy tuần tự, bạn mất khoảng 50-60 giây. Với Threading, con số này có thể giảm xuống còn 5-7 giây.

import threading
import requests
import time

# Giả lập danh sách 100 URL
urls = ["https://google.com"] * 100 

def fetch_url(url):
    requests.get(url, timeout=5)

def run_threading():
    threads = []
    for url in urls:
        t = threading.Thread(target=fetch_url, args=(url,))
        threads.append(t)
        t.start()
    for t in threads: t.join()

if __name__ == "__main__":
    start = time.time()
    run_threading()
    print(f"Threading hoàn thành trong: {time.time() - start:.2f}s")

Lưu ý: Đừng dùng Multiprocessing ở đây. Việc khởi tạo 100 process sẽ ngốn hàng GB RAM của bạn chỉ để làm một việc là… ngồi đợi web phản hồi.

Kịch bản 2: Xử lý file log 1GB (CPU-bound)

Giả sử bạn cần dùng Regex để trích xuất thông tin từ hàng triệu dòng log. CPU lúc này sẽ phải hoạt động 100% công suất.

import multiprocessing
import time

def heavy_computation(data_chunk):
    # Giả lập xử lý nặng bằng tính tổng bình phương
    return sum(i * i for i in range(10**7))

if __name__ == "__main__":
    tasks = [1, 2, 3, 4]
    
    # Chạy đa nhân
    start = time.time()
    with multiprocessing.Pool(processes=4) as pool:
        pool.map(heavy_computation, tasks)
    print(f"Multiprocessing (4 cores) tốn: {time.time() - start:.2f}s")

Trên máy core i7, Multiprocessing giúp rút ngắn thời gian gần 4 lần so với chạy vòng lặp for thông thường.

Mẹo nhỏ: Khi làm việc với Regex phức tạp, mình thường dùng Regex Tester để kiểm tra pattern trước. Việc này giúp tránh lỗi logic khi đẩy code vào các process chạy song song, vì debug đa tiến trình mệt hơn debug đơn luồng rất nhiều.

Bảng chọn nhanh (Cheat Sheet)

Đặc điểm Threading Multiprocessing
Vùng nhớ Dùng chung (Tiết kiệm RAM) Cô lập (Tốn RAM hơn)
Giao tiếp Dễ (chia sẻ biến trực tiếp) Phức tạp (cần IPC, Queue)
Độ ổn định Một thread lỗi có thể kéo cả app đi xuống Một process chết không ảnh hưởng process khác
Ưu tiên dùng khi Gọi API, Đọc/Ghi file, Query DB Xử lý số liệu, Image processing, AI/ML

3 bài học xương máu từ kinh nghiệm thực tế

  1. Tránh lạm dụng số lượng Process: Đừng tạo 100 process trên CPU 8 nhân. Việc hệ điều hành phải liên tục chuyển đổi ngữ cảnh (context switch) sẽ khiến máy bạn lag kinh khủng mà hiệu suất lại giảm.
  2. Cẩn thận với Shared State: Trong Threading, hai luồng cùng sửa một biến global sẽ tạo ra Race Condition. Hãy luôn dùng threading.Lock() để bảo vệ dữ liệu nhạy cảm.
  3. Sử dụng concurrent.futures: Đây là thư viện hiện đại giúp bạn đổi từ Thread sang Process chỉ bằng cách thay đổi một dòng code. Nó sạch sẽ và dễ quản lý hơn các module cũ.

Lời kết

Không có công cụ nào là tốt nhất, chỉ có công cụ phù hợp nhất. Nếu app của bạn dành thời gian để “chờ”, hãy chọn Threading. Nếu app cần “nghĩ”, hãy chọn Multiprocessing. Hiểu rõ bản chất của GIL sẽ giúp bạn viết code Python chuyên nghiệp và hiệu quả hơn rất nhiều.

Share: