PHP RFC ที่กำหนด Polling API แบบ native ได้รับการอนุมัติและรวมเข้ากับ master branch ของภาษาแล้ว ซึ่งช่วยให้นักพัฒนาสามารถทำ polling file descriptors ได้อย่างรวดเร็วในระดับ OS เมื่อรวมเข้ากับ Fibers ที่เพิ่งเพิ่มเข้ามาเมื่อเร็วๆ นี้ ตอนนี้ PHP จึงมีรากฐานที่มีประสิทธิภาพและติดตั้งมาในตัวสำหรับการเขียนโค้ดแบบ asynchronous ซึ่งเป็นช่องว่างที่ทำให้ไลบรารีต่างๆ ต้องพยายามสร้างวิธีแก้ปัญหาเฉพาะตัวขึ้นมาเป็นเวลานาน
Fibers และ event loop ที่แยกออกจากกัน
Fibers คือหน่วยพื้นฐานของการควบคุมลำดับการทำงาน (control-flow primitive) ระดับต่ำ พวกมันช่วยให้ฟังก์ชันสามารถระงับการทำงาน (suspend) ณ จุดที่เลือก และกลับมาทำงานต่อ (resume) จากจุดเดิมได้อย่างแม่นยำ สิ่งสำคัญคือ fiber ไม่มีความรู้เกี่ยวกับ sockets, timers หรือแหล่งข้อมูล I/O อื่นๆ เลย มันเพียงแค่หยุดพักและเริ่มทำงานใหม่ตามคำสั่งเท่านั้น
Event loop คือตัวจัดตารางเวลา (scheduler) ที่ตัดสินใจว่าควรจะให้ fiber ที่หยุดพักอยู่กลับมาทำงานต่อเมื่อใด ใน stack แบบ async ทั่วไป loop จะคอยเฝ้าดูชุดของ file descriptors รอให้พวกมันพร้อมสำหรับการอ่านหรือเขียน จากนั้นจึงปลุก fiber ที่เกี่ยวข้องให้ทำงาน
ก่อนหน้านี้ PHP มี Fibers แต่ยังขาดกลไกแบบ native ในการถามระบบปฏิบัติการว่า descriptor ใดบ้างที่พร้อมใช้งาน ผลที่ตามมาคือต้องพึ่งพา stream_select() หรือ extension ภายนอก และไลบรารี async ทุกตัวต่างก็ต้องเขียน back-ends ของตัวเองสำหรับ epoll ของ Linux, kqueue ของ BSD, IOCP ของ Windows และอื่นๆ
Polling API ใหม่นี้เข้ามาเติมเต็มส่วนที่ขาดหายไป โดยทำหน้าที่เป็น wrapper บางๆ รอบความสามารถในการ polling ของ OS (เช่น epoll บน Linux, kqueue บน BSD/macOS เป็นต้น) API นี้ไม่ได้มาแทนที่ Fibers แต่ช่วยให้ event loop มีความเร็วและความสามารถในการขยายตัว (scalability) ตามที่ต้องการ
ทำไมการเปลี่ยนแปลงนี้จึงสำคัญต่อ ReactPHP, Amp และเพื่อนๆ
ReactPHP และ Amp v3 ได้สร้างชั้นการทำงานแบบ abstraction เหนือกลไกการ polling ของ OS ไว้เอง ซึ่งชั้นเหล่านี้ประกอบด้วยเส้นทางการทำงานของโค้ด (code paths) หลายทาง โดยแต่ละทางจะถูกปรับแต่งมาเพื่อแพลตฟอร์มเฉพาะ และต้องคอยปรับปรุงให้สอดคล้องกับการเปลี่ยนแปลงของ kernel อยู่เสมอ เมื่อมี Polling API แบบ native ไลบรารีเหล่านี้จะสามารถตัดส่วนงานจัดการระบบ (plumbing) ส่วนใหญ่ทิ้งไป และหันมาใช้การเรียกใช้งานเพียงครั้งเดียวที่จัดเตรียมไว้ให้เป็นแกนหลักแทน
- Maintenance (การบำรุงรักษา) – การมีเงื่อนไขเฉพาะแพลตฟอร์มน้อยลง หมายถึงบั๊กที่น้อยลงและขอบเขตในการตรวจสอบความปลอดภัยที่เล็กลง
- Performance (ประสิทธิภาพ) – การเรียกใช้งานแบบ native จะสื่อสารโดยตรงกับ epoll/kqueue
- Portability (การพกพา) – โค้ดที่รันบน PHP แบบ "vanilla" จะได้รับประสิทธิภาพพื้นฐานที่เท่ากันในทุกระบบปฏิบัติการที่รองรับ โดยไม่จำเป็นต้องใช้ extension เสริม
ในทางตรงกันข้าม Swoole ยังคงเป็นตัวแทน runtime แบบเต็มรูปแบบที่มาพร้อมกับ event loop และระบบ coroutine ของตัวเอง Polling API ไม่ได้ส่งผลต่อการตัดสินใจเลือกใช้งาน (trade-offs) ของ Swoole นักพัฒนาที่ต้องการความหน่วงต่ำเป็นพิเศษ (ultra-low latency) หรือการจัดการหน่วยความจำแบบกำหนดเองจะยังคงพิจารณา Swoole เป็นอีกหนึ่งทางเลือกแยกต่างหาก
ผลการทดสอบ benchmark สั้นๆ บอกเล่าเรื่องราวได้ดี
ตัวจัดตารางเวลา (scheduler) แบบมินิมอลได้ดึงข้อมูล URL หลายรายการผ่าน raw sockets โดยใช้ Io\Poll\Context เพียงตัวเดียวเพื่อจัดการหลายๆ fibers ซึ่งมีข้อสังเกตสองประการเกิดขึ้น:
- Speed (ความเร็ว) – การเพิ่มคำขอที่ทำงานพร้อมกัน (concurrent requests) มากขึ้น ไม่ได้ทำให้เวลาที่ใช้ทั้งหมดเพิ่มขึ้น โดยชุดคำขอจะเสร็จสิ้นเร็วเท่ากับคำขอที่ช้าที่สุดเพียงรายการเดียว กล่าวคือ overhead ของการทำงานแบบ concurrency นั้นแทบจะเป็นศูนย์
- CPU cost (ต้นทุน CPU) – เมื่อใช้
stream_select()การใช้งาน CPU จะเพิ่มขึ้นอย่างเห็นได้ชัดเมื่อจำนวน stream เพิ่มขึ้น เนื่องจากฟังก์ชันต้องวนลูปตรวจสอบทุก descriptor ในทุกๆ การเรียกใช้งาน แต่ต้นทุนของ Polling API จะคงที่ตั้งแต่ 3 stream ไปจนถึง 30 stream ต้องขอบคุณความสามารถของ kernel ในการตรวจสอบหลาย descriptor ได้ในการเรียกใช้งานระบบ (system call) เพียงครั้งเดียว
สิ่งที่นักพัฒนาควรทำในตอนนี้
- ใช้ Amp v3 หากคุณชอบสไตล์ที่เน้น fiber-native ซึ่งสอดคล้องโดยตรงกับ API ใหม่ อินเทอร์เฟซสาธารณะของมันได้เชื่อมโยงกับ poller พื้นฐานอยู่แล้ว ดังนั้นคุณจึงได้รับประโยชน์โดยไม่ต้องเขียนโค้ดใหม่
- ใช้ ReactPHP ต่อไป หากคุณชอบการควบคุมวงจรชีวิต (lifecycle) ของ loop อย่างชัดเจน
- หลีกเลี่ยงการสร้าง scheduler เอง สำหรับงานที่ใช้งานจริง (production workloads) อย่าเขียน scheduler ของคุณเองเพื่อใช้งานในระดับ production
