Python concurrent.futures: Xử Lý Hàng Loạt Task Nhanh Hơn Với ThreadPoolExecutor Và ProcessPoolExecutor
- Home
- Python concurrent.futures: Xử Lý Hàng Loạt Task Nhanh Hơn Với ThreadPoolExecutor Và ProcessPoolExecutor
Khi chương trình Python của bạn phải xử lý hàng trăm request API, đọc ghi hàng ngàn file hoặc thực hiện các phép toán nặng, việc chạy theo cách tuần tự (sequential) sẽ tạo ra nút thắt cổ chai vô cùng lớn. Module concurrent.futures ra đời từ Python 3.2 nhằm cung cấp một interface cấp cao, giúp lập trình viên triển khai xử lý song song và bất đồng bộ một cách tối ưu, ngắn gọn.
Trong bài viết này, DCSOFT sẽ cùng bạn tìm hiểu chi tiết cách hoạt động của concurrent.futures, phân biệt khi nào nên dùng ThreadPoolExecutor hay ProcessPoolExecutor, cùng các ví dụ thực tế giúp tối ưu tốc độ ứng dụng lên gấp nhiều lần.
concurrent.futures là một thư viện chuẩn (built-in) của Python cung cấp giao diện abstraction cấp cao để thực thi các lời gọi bất đồng bộ bằng cách sử dụng các luồng (Threads) hoặc tiến trình (Processes).
So với module cấp thấp như threading hay multiprocessing, concurrent.futures mang lại nhiều ưu điểm vượt trội:
API đồng nhất: Dùng chung cấu trúc cho cả Threading và Multiprocessing.
Tự động quản lý Pool: Quản lý vòng đời của Worker, tái sử dụng tài nguyên hiệu quả mà không cần quản lý thủ công (như tạo, start, join từng thread).
Quản lý kết quả dễ dàng: Thông qua đối tượng Future, bạn dễ dàng kiểm tra trạng thái, hủy task hoặc lấy kết quả trả về khi task hoàn thành.
Để tối ưu hiệu năng trong Python, điều cốt lõi là phải xác định đúng bản chất tác vụ bạn đang xử lý. Python bị ảnh hưởng bởi GIL (Global Interpreter Lock) – cơ chế ngăn nhiều thread thực thi mã bytecode Python cùng một lúc.
┌─────────────────────────────────────────┐
│ Xác Định Loại Tác Vụ (Task Type) │
└────────────────────┬────────────────────┘
│
┌─────────────────┴─────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ I/O-Bound │ │ CPU-Bound │
│ (Đọc file, API, DB) │ │ (Xử lý ảnh, AI, Math)│
└──────────┬──────────┘ └──────────┬──────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ ThreadPoolExecutor │ │ ProcessPoolExecutor │
│ (Vượt qua GIL khi I/O) │ │(Nhiều Process riêng biệt)│
└─────────────────────────┘ └─────────────────────────┘
Tác vụ đặc trưng: Crawl data trang web, gọi API bên thứ ba, truy vấn Database, đọc/ghi file từ đĩa.
Cơ chế: Khi một Thread chờ phản hồi từ mạng hoặc đĩa (trạng thái I/O wait), GIL sẽ được giải phóng để thread khác chạy. Nhờ đó, chương trình tăng tốc rõ rệt.
Tác vụ đặc trưng: Xử lý hình ảnh/video, tính toán toán học phức tạp, huấn luyện mô hình Machine Learning, nén dữ liệu.
Cơ chế: Tạo ra các tiến trình Python hoàn toàn riêng biệt (mỗi process có một GIL độc lập và CPU core riêng). Điều này giúp tận dụng tối đa tất cả các nhân của CPU (Multi-core).
Giả sử bạn cần gửi request lấy dữ liệu từ danh sách nhiều URL. Nếu chạy tuần tự, thời gian chờ sẽ là tổng thời gian của tất cả các request.
executor.map() (Giữ đúng thứ tự kết quả)executor.map() phù hợp khi bạn muốn nhận kết quả trả về theo đúng thứ tự của danh sách đầu vào.
import time
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1"
]
def fetch_url(url):
response = requests.get(url)
return f"Done {url} - Status: {response.status_code}"
start_time = time.time()
# Sử dụng Context Manager (with) để tự động shutdown pool khi xong
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(fetch_url, urls)
for result in results:
print(result)
print(f"Tổng thời gian hoàn thành: {time.time() - start_time:.2f} giây")
Kết quả: Tổng thời gian chỉ mất khoảng 3 giây (thay vì 7 giây nếu chạy tuần tự).
executor.submit() và as_completed() (Nhận kết quả ngay khi xong)Nếu bạn muốn xử lý kết quả của task nào xong trước ngay lập tức mà không cần chờ các task khác, hãy dùng submit() kết hợp với as_completed().
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
urls = [
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2"
]
def fetch_url(url):
res = requests.get(url)
return url, res.status_code
with ThreadPoolExecutor(max_workers=3) as executor:
# Submit từng task và lưu trữ đối tượng Future
futures = [executor.submit(fetch_url, url) for url in urls]
# Lấy kết quả ngay khi có bất kỳ task nào hoàn thành
for future in as_completed(futures):
url, status = future.result()
print(f"Hoàn thành: {url} -> Status {status}")
Khi cần xử lý thuật toán tính toán nặng trên số lượng phần tử lớn, ProcessPoolExecutor là sự lựa chọn bắt buộc.
Lưu ý quan trọng trên Windows: Mã nguồn chạy
ProcessPoolExecutorbắt buộc phải nằm trong khốiif __name__ == '__main__':để tránh lỗi đệ quy tạo process.
import time
from concurrent.futures import ProcessPoolExecutor
# Hàm kiểm tra số nguyên tố (tác vụ ngốn CPU)
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
numbers = [100000007, 100000037, 100000039, 100000049, 100000081]
if __name__ == '__main__':
start = time.time()
with ProcessPoolExecutor() as executor:
results = list(executor.map(is_prime, numbers))
print(f"Kết quả: {results}")
print(f"Thời gian xử lý song song: {time.time() - start:.2f} giây")
Để tối ưu hóa hiệu năng và tránh hiện tượng cạn kiệt tài nguyên hệ thống, bạn nên áp dụng các nguyên tắc sau:
Xác định số lượng max_workers hợp lý:
Cho ThreadPoolExecutor: Số lượng worker có thể đặt từ 5 đến 30 tùy thuộc vào độ trễ của mạng/I/O.
Cho ProcessPoolExecutor: Mặc định Python sẽ lấy bằng số nhân CPU khả dụng (os.cpu_count()). Không nên đặt quá số nhân thực tế vì sẽ làm tăng chi phí Context Switching.
Cử lý ngoại lệ (Exception Handling):
Nếu một hàm ném ra Exception trong thread/process, exception đó chỉ thực sự xuất hiện khi bạn gọi future.result(). Hãy luôn bọc try...except quanh future.result().
Ưu tiên sử dụng Context Manager (with block):
Giúp tự động dọn dẹp tài nguyên và đợi các worker kết thúc (shutdown(wait=True)) trước khi thoát chương trình.
Module concurrent.futures là công cụ mạnh mẽ và dễ tiếp cận giúp các lập trình viên Python tăng tốc độ xử lý chương trình lên đáng kể. Bằng việc phân biệt rõ ràng giữa I/O-bound (dùng ThreadPoolExecutor) và CPU-bound (dùng ProcessPoolExecutor), bạn hoàn toàn có thể xây dựng những ứng dụng xử lý hàng loạt công việc một cách mượt mà và tối ưu nhất.
Nếu bạn đang phát triển các ứng dụng web phức tạp, giải pháp phần mềm doanh nghiệp hoặc hệ thống xử lý dữ liệu lớn, hãy liên hệ ngay với DCSOFT để nhận tư vấn kiến trúc phần mềm chuyên nghiệp nhất!