สรุป:
- โมดูล
subprocessของ Python และไลบรารีpsutilใช้วิธีที่ไม่มีประสิทธิภาพแบบ 'Busy-loop polling' มาเป็นเวลา 15 ปีในการรอการสิ้นสุดของโปรเซส (wait()) โดยวนเรียกsleepและwaitpidซ้ำไปมา - วิธีนี้ก่อให้เกิด CPU wake-up ที่ไม่จำเป็น การใช้แบตเตอรี่เพิ่มขึ้น ปัญหา latency ในการตรวจจับการจบของโปรเซส และมีข้อจำกัดด้าน scalability เมื่อต้องมอนิเตอร์หลายโปรเซสพร้อมกัน
- อัปเดตล่าสุดได้เพิ่มการรอแบบ 'event-driven waiting' อย่างแท้จริง โดยบน Linux ใช้
pidfd_open()และpoll()ส่วนบน BSD/macOS ใช้kqueue() - Windows ใช้
WaitForSingleObjectอยู่แล้วจึงไม่มีการเปลี่ยนแปลง แต่บนระบบ POSIX จะตัด context switching ที่ไม่จำเป็นออกไป และทำให้การใช้ CPU เข้าใกล้ '0'
สรุปแบบละเอียด:
1. ปัญหาที่ดำเนินต่อเนื่องมา 15 ปี: Busy-loop polling
ตั้งแต่มีการเพิ่มพารามิเตอร์ timeout ให้กับ subprocess.Popen.wait() ใน Python 3.3 เป็นต้นมา ไลบรารีมาตรฐานของ Python และไลบรารี psutil ที่ใช้อย่างแพร่หลาย ต่างใช้วิธีที่ไม่มีประสิทธิภาพในการรอให้โปรเซสจบการทำงาน
ตรรกะเดิมมีความเรียบง่าย แต่ไม่มีประสิทธิภาพ ดังนี้:
- ตรวจสอบสถานะโปรเซสด้วย
waitpid(WNOHANG)(non-blocking) - หากยังไม่จบ ก็
sleep()ชั่วครู่ (ใช้ exponential backoff) - กลับไปข้อ 1 และทำซ้ำ
# วิธีเดิม (โค้ดเชิงแนวคิด)
import time, os
def wait_busy(pid, timeout):
delay = 0.0001
while True:
# ตรวจว่ากระบวนการจบหรือยัง (polling)
if os.waitpid(pid, os.WNOHANG) == (pid, status):
return status
time.sleep(delay)
delay = min(delay * 2, 0.040) # เพิ่มเวลารอได้สูงสุด 40ms
วิธีนี้มีข้อเสียร้ายแรง 3 ประการดังนี้
- CPU Wake-ups: ต่อให้เพิ่มช่วงเวลารอมากขึ้น ระบบก็ยังต้องตื่นขึ้นมาเป็นระยะเพื่อตรวจสอบสถานะอยู่ดี ทำให้เปลือง CPU cycle และใช้พลังงานเพิ่ม
- Latency (ความหน่วง): ย่อมมีช่วงเวลาคลาดเคลื่อนระหว่างเวลาที่โปรเซสจบจริง กับเวลาที่ระบบตื่นจาก
sleepมาตรวจพบ - Scalability (ความสามารถในการขยาย): ในสภาพแวดล้อมเซิร์ฟเวอร์ที่ต้องมอนิเตอร์โปรเซสพร้อมกันเป็นหลักร้อยหรือหลักพัน overhead แบบนี้จะเพิ่มขึ้นอย่างรวดเร็ว
2. วิธีแก้: การรอแบบ event-driven สำหรับระบบ POSIX
ระบบ POSIX ทุกตัวมีกลไกสำหรับตรวจจับการเปลี่ยนแปลงสถานะของ file descriptor (select, poll, epoll, kqueue) อยู่แล้ว ล่าสุด Python และ psutil ได้ปรับปรุงให้ใช้กลไกเหล่านี้กับการตรวจจับ PID ของโปรเซส
- Linux: ใช้ system call
pidfd_open()ซึ่งถูกเพิ่มเข้ามาในเคอร์เนล Linux 5.3 เมื่อปี 2019 โดยมันจะคืนค่า file descriptor ที่อ้างถึง PID ของโปรเซส จากนั้นสามารถนำไปลงทะเบียนกับpoll()หรือepoll()เพื่อเฝ้าดู event การสิ้นสุดของโปรเซสได้ (ถูกเพิ่มในโมดูลosของ Python ตั้งแต่ 3.9) - BSD / macOS: ใช้ฟิลเตอร์
EVFILT_PROCของ system callkqueue()เพื่อติดตาม event ของโปรเซสอย่างมีประสิทธิภาพ - Windows: รองรับการรอแบบ event-driven อยู่แล้วผ่าน API
WaitForSingleObjectจึงไม่มีการเปลี่ยนแปลง
3. การปรับปรุงประสิทธิภาพและผลลัพธ์
จากการเปลี่ยนแปลงนี้ เมื่อมีการเรียก wait() โปรเซสจะอยู่ในสถานะ 'Interruptible sleep' จากมุมมองของเคอร์เนล กล่าวคือ แทบไม่ใช้ CPU เลย และจะรออยู่เงียบ ๆ ใน kernel space จนกระทั่งมีสัญญาณว่าโปรเซสสิ้นสุด แล้วจึงตื่นขึ้นทันที
ผลการ benchmark ด้วย /usr/bin/time -v และเครื่องมืออื่น ๆ แสดงให้เห็นว่า เมื่อเทียบกับวิธีเดิม จำนวน context switching ที่ไม่จำเป็นลดลงอย่างมาก และความเร็วในการตรวจจับการสิ้นสุดของโปรเซสดีขึ้นแบบทันที อัปเดตนี้ถูกรวมเข้าไปทั้งในไลบรารี psutil และแกนหลักของ CPython แล้ว ทำให้นักพัฒนา Python จะได้รับประโยชน์ด้านประสิทธิภาพนี้ในอนาคตโดยไม่ต้องแก้โค้ดเพิ่มเติมเอง
ยังไม่มีความคิดเห็น