Tóm tắt:
- Mô-đun
subprocesscủa Python và thư việnpsutiltrong suốt 15 năm qua đã dùng cách "thăm dò Busy-loop" kém hiệu quả, lặp lạisleepvàwaitpidkhi chờ tiến trình kết thúc (wait()). - Cách này gây ra các vấn đề như CPU bị đánh thức không cần thiết, hao pin, độ trễ khi phát hiện tiến trình kết thúc, và khả năng mở rộng kém khi giám sát nhiều tiến trình.
- Qua các bản cập nhật gần đây, trên Linux đã triển khai "chờ hướng sự kiện" thực thụ bằng
pidfd_open()vàpoll(), còn trên BSD/macOS làkqueue(). - Windows vốn đã dùng
WaitForSingleObjectnên không có thay đổi, nhưng trên các hệ POSIX, việc chuyển ngữ cảnh không cần thiết được loại bỏ và mức dùng CPU tiến gần về '0'.
Tóm tắt chi tiết:
1. Vấn đề kéo dài 15 năm: thăm dò Busy-loop
Kể từ khi tham số timeout được thêm vào subprocess.Popen.wait() ở Python 3.3, thư viện chuẩn Python và thư viện psutil được dùng rộng rãi đã sử dụng một cách tiếp cận kém hiệu quả để chờ tiến trình kết thúc.
Logic cũ đơn giản nhưng thiếu hiệu quả như sau:
- Kiểm tra trạng thái tiến trình bằng
waitpid(WNOHANG)(không chặn) - Nếu chưa kết thúc thì
sleep()một lúc ngắn (áp dụng exponential backoff) - Quay lại bước 1 và lặp lại
# Cách cũ (mã khái niệm)
import time, os
def wait_busy(pid, timeout):
delay = 0.0001
while True:
# Kiểm tra tiến trình đã kết thúc chưa (polling)
if os.waitpid(pid, os.WNOHANG) == (pid, status):
return status
time.sleep(delay)
delay = min(delay * 2, 0.040) # Tăng thời gian chờ lên tối đa 40ms
Cách này có 3 nhược điểm nghiêm trọng sau.
- CPU Wake-ups: Dù có tăng thời gian chờ đến đâu thì hệ thống vẫn phải thức dậy theo chu kỳ để kiểm tra trạng thái, gây lãng phí chu kỳ CPU và điện năng.
- Latency (độ trễ): Luôn tồn tại một khoảng chênh thời gian giữa lúc tiến trình thực sự kết thúc và lúc hệ thống thức dậy khỏi
sleepđể phát hiện điều đó. - Scalability (khả năng mở rộng): Trong môi trường máy chủ phải giám sát hàng trăm hoặc hàng nghìn tiến trình đồng thời, overhead này sẽ tăng rất nhanh.
2. Giải pháp: chờ hướng sự kiện cho các hệ POSIX
Mọi hệ POSIX đều cung cấp cơ chế phát hiện thay đổi trạng thái của file descriptor (select, poll, epoll, kqueue). Gần đây Python và psutil đã được cải tiến để tận dụng các cơ chế này cho việc theo dõi PID của tiến trình.
- Linux: Sử dụng system call
pidfd_open()được giới thiệu trong nhân Linux 5.3 vào năm 2019. Nó trả về một file descriptor trỏ tới PID của tiến trình, sau đó có thể đăng ký vớipoll()hoặcepoll()để theo dõi sự kiện tiến trình kết thúc. (Được thêm vào mô-đunostừ Python 3.9) - BSD / macOS: Dùng bộ lọc
EVFILT_PROCcủa system callkqueue()để giám sát sự kiện tiến trình một cách hiệu quả. - Windows: Đã hỗ trợ chờ hướng sự kiện thông qua API
WaitForSingleObject, nên không có thay đổi.
3. Cải thiện hiệu năng và kết quả
Nhờ thay đổi này, khi gọi wait(), tiến trình sẽ ở trạng thái "Interruptible sleep" theo góc nhìn của kernel. Tức là, không tiêu tốn CPU chút nào, nó chờ yên lặng trong không gian kernel và sẽ thức dậy ngay lập tức khi có tín hiệu tiến trình kết thúc.
Kết quả benchmark bằng /usr/bin/time -v cho thấy so với cách cũ, số lần chuyển ngữ cảnh (Context Switching) không cần thiết đã giảm mạnh, đồng thời tốc độ phát hiện tiến trình kết thúc cũng được cải thiện gần như tức thì. Bản cập nhật này đã được đưa vào thư viện psutil và lõi CPython, nên từ nay các nhà phát triển Python có thể hưởng lợi về hiệu năng mà không cần sửa mã riêng.
Chưa có bình luận nào.