12 คะแนน โดย darjeeling 2026-01-31 | ยังไม่มีความคิดเห็น | แชร์ทาง WhatsApp

สรุป:

  • โมดูล 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 ที่ใช้อย่างแพร่หลาย ต่างใช้วิธีที่ไม่มีประสิทธิภาพในการรอให้โปรเซสจบการทำงาน

ตรรกะเดิมมีความเรียบง่าย แต่ไม่มีประสิทธิภาพ ดังนี้:

  1. ตรวจสอบสถานะโปรเซสด้วย waitpid(WNOHANG) (non-blocking)
  2. หากยังไม่จบ ก็ sleep() ชั่วครู่ (ใช้ exponential backoff)
  3. กลับไปข้อ 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 call kqueue() เพื่อติดตาม event ของโปรเซสอย่างมีประสิทธิภาพ
  • Windows: รองรับการรอแบบ event-driven อยู่แล้วผ่าน API WaitForSingleObject จึงไม่มีการเปลี่ยนแปลง

3. การปรับปรุงประสิทธิภาพและผลลัพธ์
จากการเปลี่ยนแปลงนี้ เมื่อมีการเรียก wait() โปรเซสจะอยู่ในสถานะ 'Interruptible sleep' จากมุมมองของเคอร์เนล กล่าวคือ แทบไม่ใช้ CPU เลย และจะรออยู่เงียบ ๆ ใน kernel space จนกระทั่งมีสัญญาณว่าโปรเซสสิ้นสุด แล้วจึงตื่นขึ้นทันที

ผลการ benchmark ด้วย /usr/bin/time -v และเครื่องมืออื่น ๆ แสดงให้เห็นว่า เมื่อเทียบกับวิธีเดิม จำนวน context switching ที่ไม่จำเป็นลดลงอย่างมาก และความเร็วในการตรวจจับการสิ้นสุดของโปรเซสดีขึ้นแบบทันที อัปเดตนี้ถูกรวมเข้าไปทั้งในไลบรารี psutil และแกนหลักของ CPython แล้ว ทำให้นักพัฒนา Python จะได้รับประโยชน์ด้านประสิทธิภาพนี้ในอนาคตโดยไม่ต้องแก้โค้ดเพิ่มเติมเอง

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น