• มีการตั้งข้อสังเกตว่าเป็นเรื่องน่าเสียดายที่ตำรา The Art of Multiprocessor Programming ไม่กล่าวถึงแนวคิด futex
  • Futex เป็นองค์ประกอบหลักของ การซิงโครไนซ์ที่มีประสิทธิภาพ ในการเขียนโปรแกรมแบบขนานสมัยใหม่ และให้ประสิทธิภาพดีกว่าล็อกแบบ System V เดิม
  • futex มีโครงสร้างที่ แยกการได้มาของล็อกออกจากการรอ/การปลุก เพื่อลด system call และ overhead ที่ไม่จำเป็น
  • มีตัวอย่างและคำอธิบายเทคนิคในการนำ futex ไปใช้สร้าง concurrency primitives หลากหลายแบบด้วยตนเอง เช่น spinlock, mutex, และ recursive lock
  • ผู้เขียนชี้ว่าหนังสือไม่ได้ครอบคลุม วิธีการซิงโครไนซ์สมัยใหม่ที่จำเป็นต่อการทำงานวิศวกรรมจริง สะท้อนช่องว่างระหว่างแวดวงวิชาการกับภาคปฏิบัติ

บทนำ

  • Phil Eaton เริ่มต้นชมรมอ่านหนังสือ 'The Art of Multiprocessor Programming, 2nd Edition'
  • แม้หนังสือเล่มนี้จะถูกมองว่าเป็นตำราทรงอิทธิพลในด้านการเขียนโปรแกรมแบบขนาน แต่ผู้เขียนชี้ถึง การขาดความเป็นประโยชน์เชิงปฏิบัติ ของเนื้อหา
  • โดยเฉพาะอย่างยิ่ง หนังสือระบุว่ามุ่งเป้าไปที่นักศึกษาปริญญาตรีปี 4 และนักศึกษาบัณฑิตศึกษา แต่กลับไม่กล่าวถึง futex ซึ่งเป็น เทคนิคการซิงโครไนซ์หลัก

futex คืออะไร – ทำไมจึงสำคัญ

  • futex ย่อมาจาก “fast user space mutex” แต่ในความเป็นจริงแล้วมันไม่ใช่ mutex โดยตรง หากเป็น primitive สำหรับการซิงโครไนซ์ที่ระบบปฏิบัติการสนับสนุน เพื่อใช้สร้างล็อกสมัยใหม่
  • ในอดีต ล็อกส่วนใหญ่สร้างบน semaphore ของ System V IPC ทำให้มี ข้อจำกัดด้านประสิทธิภาพและการขยายตัว
  • เมื่อ Linux นำ futex มาใช้ในปี 2002 ก็แสดงให้เห็นว่า เร็วกว่า System V lock ถึง 20~120 เท่าในสภาพแวดล้อมที่มีงานพร้อมกัน 1000 งาน
  • ระบบปฏิบัติการอื่นอย่าง Windows (ปี 2012) และ macOS (ปี 2016) ก็ได้นำกลไกคล้ายกันมาใช้
  • ปัจจุบัน ล็อกใน system library ที่ใช้งานแพร่หลายอย่าง pthreads ก็ใช้ futex

หลักการทำงานและจุดแตกต่างของ futex

  • semaphore แบบเดิมผูกการล็อกเข้ากับการรอ แต่ futex แยกการได้มาของล็อกออกจากการรอ/การปลุก
  • ด้วยเหตุนี้จึงลด ดีเลย์และ system call ที่ไม่จำเป็น ได้ และหากมั่นใจว่าไม่มีเธรดที่รออยู่ในตอนปลดล็อก ก็ไม่จำเป็นต้องเข้าสู่เคอร์เนล
  • การเรียก wait ของ futex จะทำให้ “รอเฉพาะเมื่อค่าที่ address หน่วยความจำหนึ่งอยู่ในสถานะที่ต้องการเท่านั้น” และยังรองรับ timeout
  • การเรียก wake ของ futex จะปลุกจำนวนเธรดตามต้องการจากรายการรอภายในที่ผูกกับ address หน่วยความจำหนึ่ง
  • มัน กำหนดให้ตรวจสอบค่าจริงของ address หน่วยความจำ เพื่อลดการรอที่ไม่จำเป็นเมื่อสถานะเปลี่ยนไปแล้ว

การใช้งาน futex ในทางปฏิบัติ – ลงมือสร้างเอง

  • futex เป็น primitive ระดับล่าง ดังนั้นจึงต้องใช้ชนิดข้อมูล atomic โดยคำนึงถึง ประเด็นลำดับการทำงานของหน่วยความจำจากคอมไพเลอร์และฮาร์ดแวร์
  • บน Linux ต้องเรียก futex system call โดยตรงผ่าน syscall ส่วนบน macOS ใช้อินเทอร์เฟซ __ulock (ปัจจุบันมี API ที่ใช้ง่ายกว่านี้เพิ่มเข้ามาแล้ว)
  • โดยพื้นฐานแล้ว การรอด้วย futex จะคืนค่า 0 เมื่อสำเร็จ และคืน error code เมื่อไม่สำเร็จ เช่น timeout
  • โอเปอเรชันหลักที่อิงกับ futex:
    • h4x0r_futex_wait_timespec() : รอเมื่อค่าที่คาดไว้ตรงกัน และสามารถกำหนด timeout ได้
    • h4x0r_futex_wake() : ปลุกผู้รอ 1 รายหรือทั้งหมด

ตัวอย่างใช้งานจริงของการสร้าง mutex/spinlock/recursive lock

spinlock

  • ล็อกแบบพื้นฐานที่สุด ทำงานด้วย บิตเดียว (atomic_fetch_or)
  • จะวนลูปไม่สิ้นสุด (“spin”) จนกว่าจะได้ล็อก แต่ในกรณีที่มีการแข่งขันสูงจะเกิด การสิ้นเปลือง CPU และยังมีปัญหาเชิงโครงสร้าง เช่น ปลดล็อกผิด และเสี่ยง deadlock เมื่อเรียกซ้ำแบบ recursive

hybrid mutex (‘unsafe’ mutex)

  • โดยทั่วไปจะ ลองด้วย spinlock ก่อน และหากล้มเหลวจำนวนหนึ่งครั้งจึงสลับไปใช้ futex เพื่อบล็อกอย่างมีประสิทธิภาพ
  • หากไม่มีผู้รอ ก็หลีกเลี่ยง system call ที่ไม่จำเป็นได้ และในกรณีมีผู้รอก็สามารถลดจำนวน wake system call ให้น้อยที่สุด
  • เนื่องจากยังขาดการตรวจสอบ ownership และการจัดการ recursive อย่างเคร่งครัด จึงใช้ชื่อว่า “unsafe”

mutex แบบนับจำนวนผู้รอ

  • ใช้หนึ่งบิตสำหรับสถานะล็อก และใช้บิตที่เหลือสำหรับ นับจำนวนผู้รอ เพื่อลด wake system call ที่ไม่จำเป็น
  • อย่างไรก็ตาม ยังไม่มีการจัดการ ownership และ recursive

mutex ที่รวมการจัดการ ownership

  • ใช้ค่า pthread_t เพื่อ ติดตามเจ้าของล็อกและสถานะได้อย่างชัดเจน ทำให้ตรวจจับปัญหาเมื่อ unlock ผิดหรือใช้งานแบบ recursive ได้
  • ทั้งการได้มาของล็อก การปลดล็อก และการจัดการผู้รอ ล้วนควบคุมด้วย atomic operation อย่างเคร่งครัด

recursive lock

  • เพิ่ม ตัวนับจำนวนการซ้อนกัน (depth) แยกตามเธรด เพื่อให้เธรดเดียวกันสามารถได้ล็อกซ้ำแบบซ้อนได้
  • เมื่อ unlock จะลดค่า depth และเมื่อเป็น 0 จึงปลดล็อกจริงพร้อมปลุกผู้รอ
  • แต่ละการทำงานถูกสร้างขึ้นด้วย atomic operation และการตรวจสอบ ownership อย่างเคร่งครัด

โจทย์ที่ยังเหลือและความเป็นจริงของงานวิศวกรรม

  • หากเธรดเจ้าของล็อกจบการทำงานอย่างผิดปกติหรือหยุดไป จะต้องมี รายการจัดการแยกต่างหาก, callback ตอนสิ้นสุด ฯลฯ เพิ่มเติมเพื่อดูแลล็อก
  • แม้แต่เมื่อใช้ mutex ที่แชร์ข้ามโปรเซส ก็ยังต้องคำนึงถึงการจัดการการเปลี่ยนแปลงสถานะเพิ่มเติม
  • POSIX RW lock ไม่ได้กำหนดพฤติกรรมการซ้อนแบบ recursive ไว้อย่างชัดเจน และยังต่างกันไปในแต่ละ implementation ทำให้รับประกันความปลอดภัยได้ยากในทางปฏิบัติ
  • ผู้เขียนวิจารณ์ว่าหนังสือไม่ได้รวม ประเด็น concurrency ที่สำคัญจริงในการใช้งานจริง (futex, recursive lock, async runtime ฯลฯ) ไว้ในหลักสูตร

บทสรุป

  • 'The Art of Multiprocessor Programming' เอนเอียงไปทางประวัติศาสตร์หรือทฤษฎี จนไม่สามารถถ่ายทอดความรู้ภาคปฏิบัติที่สำคัญของการเขียนโปรแกรมแบบขนานสมัยใหม่ได้อย่างเพียงพอ
  • หากไม่อธิบาย องค์ประกอบการซิงโครไนซ์หลักอย่าง futex ที่ระบบใช้งานจริงอย่างเหมาะสม ก็อาจก่อผลเสียในทางปฏิบัติต่อผู้เรียนรุ่นต่อไป
  • ผู้เขียนเน้นย้ำถึงความจำเป็นในการสะท้อนแนวคิดสมัยใหม่และเสริมเนื้อหาให้ใช้งานได้จริง

เอกสารอ้างอิง

  • ตัวอย่างโค้ดทั้งหมดดูได้ที่ codeberg

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

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