2 คะแนน โดย GN⁺ 2025-10-28 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ผู้เขียนเริ่มทดลองแนวทางใหม่หลังจากเรียนรู้ ภาษา Zig ไปพร้อมกับทำโปรเจ็กต์เขียนดัชนีของ AcoustID ขึ้นใหม่ และพบข้อจำกัดของงานเขียนโปรแกรมเครือข่าย
  • เพื่อให้ได้ โมเดล asynchronous I/O และ concurrency แบบที่เคยใช้ใน C++ และ Go บน Zig ด้วย ผู้เขียนจึงตัดสินใจพัฒนาไลบรารีขึ้นเอง
  • ผลลัพธ์คือ ไลบรารี Zio ที่นำโมเดล concurrency สไตล์ Go มาปรับใช้กับ Zig ทำให้เขียนโค้ด async ที่ดูเหมือน synchronous ได้โดยไม่ต้องใช้ callback
  • Zio รองรับ asynchronous network/file I/O, channel, synchronization primitives, signal watching และให้ประสิทธิภาพในโหมด single-thread เร็วกว่า Go หรือ Tokio ของ Rust
  • โปรเจ็กต์นี้แสดงให้เห็นถึง ความเป็นไปได้ในการผสานประสิทธิภาพระดับระบบของ Zig เข้ากับโมเดล concurrency สมัยใหม่ และถูกมองว่าเป็นจุดเปลี่ยนสำคัญของการขยายระบบนิเวศ Zig

ภาษา Zig และแรงจูงใจช่วงแรก

  • เดิมทีผู้เขียนเฝ้าดู Zig ซึ่งถูกออกแบบมาเป็น ภาษาระดับล่างสำหรับซอฟต์แวร์เสียง อยู่แล้ว แต่ยังไม่รู้สึกว่าจำเป็นต้องใช้จริง
    • เขาเริ่มสนใจหลังเห็นกรณีที่ Andrew Kelley ผู้สร้าง Zig นำอัลกอริทึม Chromaprint ของผู้เขียนไปเขียนใหม่ด้วย Zig
  • ผู้เขียนใช้โปรเจ็กต์ เขียน reverse index ของ AcoustID ขึ้นใหม่ เป็นโอกาสในการเรียนรู้ Zig และสุดท้ายก็ได้งานพัฒนาที่ เร็วกว่าและขยายระบบได้ดีกว่าเวอร์ชัน C++
  • แต่เมื่อถึงขั้นเพิ่มอินเทอร์เฟซฝั่งเซิร์ฟเวอร์ ก็พบปัญหา การรองรับ asynchronous networking ที่ยังไม่เพียงพอ

แนวทางเดิมและข้อจำกัด

  • ในเวอร์ชัน C++ ก่อนหน้า ผู้เขียนใช้ เฟรมเวิร์ก Qt เพื่อจัดการ asynchronous I/O แม้จะเป็นแบบ callback-based แต่ก็ใช้งานได้เพราะมีฟังก์ชันรองรับครบถ้วน
  • ต่อมาในต้นแบบ ผู้เขียนใช้ ความสะดวกของ networking และ concurrency ในภาษา Go แต่ใน Zig ยังไม่มี abstraction ระดับใกล้เคียงกัน
  • หากจะสร้าง TCP server และชั้นคลัสเตอร์ ด้วย Zig จะเกิดความไม่มีประสิทธิภาพจากการต้องสร้างหลายเธรดจำนวนมาก
    • เพื่อแก้ปัญหานี้ ผู้เขียนจึงลงมือเขียน ไคลเอนต์ Zig สำหรับระบบส่งข้อความ NATS (nats.zig) เอง และได้สำรวจความสามารถด้าน networking ของ Zig อย่างลึกซึ้ง

การมาของไลบรารี Zio

  • จากประสบการณ์เหล่านี้ ผู้เขียนจึงเผยแพร่ Zio: ไลบรารี asynchronous I/O และ concurrency สำหรับ Zig
  • Zio มีเป้าหมายเพื่อให้ เขียนโค้ด async ได้โดยไม่ต้องพึ่ง callback โดยภายในยังคงทำงานแบบ asynchronous I/O แต่โครงสร้างภายนอกจะดูเหมือน synchronous
  • มีการนำ โมเดล concurrency สไตล์ Go มาปรับใช้กับ Zig ในรูปแบบที่มีข้อจำกัดบางส่วน
    • task ของ Zio อยู่ในรูปแบบ stackful coroutine ที่มี fixed-size stack
    • เมื่อเรียก stream.read() งาน I/O จะทำงานอยู่เบื้องหลัง และเมื่อเสร็จแล้ว task จะถูก resume เพื่อคืนผลลัพธ์
  • วิธีนี้ช่วยทั้ง ลดความซับซ้อนของการจัดการสถานะ และ เพิ่มความอ่านง่ายของโค้ด

องค์ประกอบฟังก์ชันและโครงสร้างรันไทม์

  • Zio รองรับ asynchronous network และ file I/O แบบครบถ้วน, synchronization primitives (mutex, condition variable ฯลฯ), channel สไตล์ Go, การเฝ้าดูสัญญาณจาก OS เป็นต้น
  • task สามารถทำงานได้ทั้งใน โหมด single-thread และ multi-thread
    • ในโหมด multi-thread task สามารถย้ายข้ามเธรดได้ ช่วย ลด latency และเพิ่มสมดุลของโหลด
  • มีการรองรับ อินเทอร์เฟซ Reader/Writer มาตรฐาน เพื่อให้เข้ากันได้กับไลบรารีภายนอก

ประสิทธิภาพและการเปรียบเทียบ

  • ผู้เขียนยังไม่ได้เผยแพร่ benchmark อย่างเป็นทางการ แต่ระบุว่าได้ยืนยันแล้วว่า ในโหมด single-thread เร็วกว่า Go และ Tokio ของ Rust
  • ต้นทุนของ context switching ต่ำถึงระดับการเรียกฟังก์ชัน ทำให้ความเร็วในการสลับแทบใกล้เคียงกับไม่มีต้นทุน
  • โหมด multi-thread ยังไม่แข็งแกร่งเท่า Go/Tokio แต่ก็ให้ ประสิทธิภาพใกล้เคียงกันหรือเร็วกว่าเล็กน้อย
    • ในอนาคต หากเพิ่มฟังก์ชัน fairness ก็อาจทำให้ประสิทธิภาพลดลงบางส่วน

โค้ดตัวอย่างและการใช้งาน

  • ในเอกสารมี โค้ดตัวอย่าง HTTP server ที่สร้างบน Zio รวมอยู่ด้วย
    • ใช้ zio.net.Stream เพื่อรับการเชื่อมต่อ และจัดการแต่ละการเชื่อมต่อใน task แยกกัน
    • zio.Runtime ทำหน้าที่ดูแลการรัน task และการจัดตาราง I/O
  • โครงสร้างนี้ช่วยให้ เขียน asynchronous I/O ได้เหมือนโค้ด synchronous พร้อมทั้งทำให้ ควบคุมลำดับการทำงานและการคืนทรัพยากรได้อย่างชัดเจน

แผนต่อไปและความสำคัญ

  • ผู้เขียนมองว่า Zio แสดงให้เห็นว่า Zig ไม่ได้เป็นเพียง ภาษาสำหรับโค้ดระบบประสิทธิภาพสูง เท่านั้น แต่ยังพัฒนาไปเป็น ภาษาสำหรับสร้างแอปพลิเคชันเครือข่ายอย่างสมบูรณ์ ได้ด้วย
  • ขั้นถัดไปคือการ เขียนไคลเอนต์ NATS ใหม่บน Zio และมีแผนพัฒนา ไลบรารี HTTP client/server ที่อิงกับ Zio
  • โปรเจ็กต์นี้ถูกมองว่าเป็นความพยายามสำคัญในการ ขยายโครงสร้างพื้นฐานด้าน networking และ concurrency ของระบบนิเวศ Zig และเป็นความพยายามสร้าง โมเดลรันไทม์สมัยใหม่ที่เทียบชั้นกับ Go หรือ Rust

1 ความคิดเห็น

 
GN⁺ 2025-10-28
ความคิดเห็นจาก Hacker News
  • แม้จะบอกว่าการสลับคอนเท็กซ์แทบจะ ฟรีเกือบทั้งหมด ในระดับการเรียกฟังก์ชัน แต่ในความเป็นจริงก็ยังมีต้นทุนเล็กๆ น้อยๆ เช่น branch predictor พัง ยังไม่แน่ใจว่าดีไซน์ async ของ Zig ใช้คู่ call/return ของฮาร์ดแวร์ หรือถูกแปลงเป็นแบบ indirect jump ถ้าจะทำเบนช์มาร์กให้สมบูรณ์ ต้องเปรียบเทียบเวลารันรวมของโปรแกรมที่มีการสลับอย่างต่อเนื่องระหว่างสองงาน กับโปรแกรมที่ synchronous ทั้งหมด ซึ่งทำได้ค่อนข้างยาก
    • ใน stackless coroutine ถ้าสลับสองงานต่อเนื่องกันที่ก้นของ call stack และโค้ดสลับสแตกถูก inline ก็สามารถหลีกเลี่ยง ค่าปรับจาก call/ret ไม่ตรงกัน ได้เป็นส่วนใหญ่ ถ้าควบคุมคอมไพเลอร์ได้ ก็อาจเปลี่ยน call/ret ในโค้ด I/O ให้เป็น explicit jump ได้ด้วย ในระยะยาว อยากให้ CPU มี meta-predictor เพื่อทำนาย stackful coroutine ได้ดีขึ้น
    • ตอนนี้ Zig เอา async ระดับภาษาออกไปแล้ว และผู้เขียนโพสต์เป็นคนลงมือทำ การสลับทาสก์ใน user space เอง
    • เคยทดสอบแบบ ping-pong ง่ายๆ ระหว่าง coroutine แล้วได้ตัวเลขที่แทบไม่น่าเชื่อเมื่อเทียบกับโซลูชันอื่น
    • อีกไม่นาน Zig จะมี async แบบใหม่เพิ่มเข้ามา เลยกำลังรออยู่ก่อนจะลงลึกจริงจัง บทความที่เกี่ยวข้อง: Zig new async I/O
  • Stackful coroutine มีความหมายเมื่อมี RAM มากพอ ฉันใช้ Zig อยู่ในงาน embedded (ARM Cortex-M4, RAM 256KB) และใช้มันเพื่อให้ได้ memory safety ในการทำงานร่วมกับ C ฉันชอบ async แบบมีสีของ Rust มากกว่า มันให้ความรู้สึกมหัศจรรย์ที่โค้ดดูเหมือน synchronous แต่ปัญหาคือในโค้ดเบสขนาดใหญ่จะแยกยากว่าฟังก์ชันไหน blocking
    • ที่จริงแล้วโค้ด synchronous ทั้งหมดก็เป็นแค่ ภาพลวงตา (illusion) ที่ซอฟต์แวร์สร้างขึ้น CPU ไม่ได้บล็อกจริงกับ I/O และ OS thread เองก็เป็น stackful coroutine ที่ OS ทำขึ้น ในระดับภาษาเราก็แค่ทำภาพลวงตานี้ให้มีประสิทธิภาพขึ้น แต่แก่นแท้ก็เหมือนเดิม
    • Zig IO แบบใหม่จะเป็นโครงสร้าง colored ที่ประณีตกว่า Rust สีของฟังก์ชันจะขึ้นกับว่ามันทำ I/O หรือไม่ และตอนเรียกจะระบุชัดว่าเป็น async หรือไม่ Zig ยังกำลังตั้งเป้าให้คำนวณขนาดสแตกที่ต้องใช้ตอนเรียกฟังก์ชันได้ด้วย จึงคาดว่าจะช่วยลด ปัญหาสิ้นเปลือง RAM ของ stackful coroutine ได้
    • เพราะอย่างนั้นเอง Zig ถึงอยากแสดง I/O แบบ explicit เพื่อให้ ติดตามได้ว่าฟังก์ชันไหนเป็น blocking
  • มีความเห็นว่าการนำ Zig มาใช้ตอนนี้ยังเร็วเกินไป เพราะโมเดล I/O กำลังเปลี่ยนครั้งใหญ่ และดูเหมือนจะต้องใช้เวลาอีกหลายปี
    • ฉันเองก็เลิกใช้ Zig ไปในปี 2020 ด้วยเหตุผลคล้ายกัน แต่โปรเจกต์ยังคงเดินหน้าอย่างคึกคัก และฉันมองในแง่ดีที่มันให้ความสำคัญกับ การออกแบบที่ถูกต้องมากกว่าการรีบออกเวอร์ชัน ตอนนี้เลยใช้ Go หรือ C ไปก่อนแล้วรอ 1.0
    • ไม่กี่ปีก็ผ่านไปเร็วมาก Zig เป็นภาษาที่ใช้งานได้ดีพอแล้ว ใครจะใช้ก็ใช้ ใครไม่ใช่ก็ไม่ใช้
    • ถ้าดูตามความเป็นจริง ตอนนี้ก็เป็นช่วงเวลาที่ไม่ดีจริงๆ เพราะ 0.16 มีการเปลี่ยน I/O ครั้งใหญ่รออยู่ และแม้แต่ผู้เขียนเองก็ยังไม่ได้ใช้ฟีเจอร์ล่าสุด ฉันเองก็จะรอ 0.16 สำหรับงานที่เน้น I/O เหมือนกัน
    • แต่ถ้าเป็นงานเกี่ยวกับ I/O การใช้ buffered reader/writer interface ของ Zig 0.15 ก็คงไม่ต้องเปลี่ยนอะไรเยอะ
    • ฉันกลับมองว่าตอนนี้ไม่ใช่ช่วงแย่ Zig ไม่ได้เปลี่ยนตัวภาษาอย่างรุนแรง แต่กำลังเพิ่ม std.Io API ใหม่ที่ทรงพลัง โค้ดเดิมยังทำงานได้ตามเดิม และ API ใหม่ก็ ใช้งานสบายขึ้นและประสิทธิภาพดีกว่า ฉันเองก็ย้ายโปรเจกต์เดิมไปใช้ Reader/Writer API ใหม่แล้ว โค้ดสะอาดขึ้นมาก
  • ยัง สงสัย อยู่ดีว่าทำไม async แบบ callback ถึงกลายเป็นมาตรฐาน แนวทางแบบ libtask ดูสะอาดกว่ามาก Rust เองก็เลือก async แบบ callback ด้วย แต่ก็ไม่เข้าใจเหตุผลนัก อ้างอิง: libtask
    • stackless coroutine สามารถทำในระดับภาษาได้ และมีข้อดีคือ โต้ตอบกับฟีเจอร์เดิมได้อย่างคาดเดาได้ แต่ถ้าจัดการสแตกเองก็อาจชนกับ exception handling, GC, debugger ฯลฯ และการรวมการเปลี่ยนแปลงแบบนี้เข้าไปในระดับ LLVM ก็ทำได้ยาก ดังนั้นสำหรับผู้ออกแบบภาษาแล้วจึงมี ข้อจำกัดในทางปฏิบัติ มาก
    • ผลการศึกษาที่ Microsoft ทำสำหรับมาตรฐาน C++ สรุปว่า stackless coroutine มี overhead ด้านหน่วยความจำน้อยกว่ามาก และเปิดอิสระในการออกแบบ executor ได้สูงกว่า
    • ข้อเสียของแนวทางแบบ zio หรือ libtask คือจำเป็นต้อง ประมาณขนาดสแตกเอง ถ้าเล็กเกินไปก็ overflow ถ้าใหญ่เกินไปก็เปลืองหน่วยความจำ ขนาดสแตกที่ต้องใช้ยังต่างกันไปในแต่ละแพลตฟอร์ม ทำให้มีปัญหาเรื่อง portability ด้วย ถ้า Zig issue #157 ถูกแก้ แนวทางนี้ก็น่าจะดีขึ้น
    • ในกรณีแบบ libtask ขนาดสแตกของเธรดก็คลุมเครือ และใหญ่กว่าสถานะ async ทั่วไปมาก
    • async ของ Rust ไม่ใช่ callback แต่เป็นแบบ polling พูดอีกอย่างคือมีสามแนวทางในการทำ async
      1. แบบ callback (Node.js, Swift)
      2. แบบ stackful (Go, libtask)
      3. แบบ polling (Rust) Rust จะถูกแปลงเป็น state machine แบบ static แล้วให้รันไทม์คอย poll แบบ stackful เปลืองหน่วยความจำมากและจัดการขนาดสแตกได้ยาก Rust เลยเลือกโครงสร้าง stackless เพื่อหลีกเลี่ยงปัญหานี้ และ Zig วางแผนจะเปิดให้เลือกใช้ได้ทั้งสองแบบ อ้างอิง: โค้ด coroutine ของ zio
  • การอ่าน TCP อาจค้างได้นานเป็นเดือน เลยสงสัยว่าอินเทอร์เฟซ I/O timeout จะเป็นอย่างไร
    • บน TCP socket สามารถตั้ง read/write timeout ได้ด้วย setsockopt Zig มีเลเยอร์ POSIX API ให้ใช้งาน อ้างอิง: เอกสาร setsockopt
    • ตอนนี้ std.Io.Reader ของ Zig ยังไม่รู้จัก timeout กำลังคิดโครงสร้างที่ทำงานคล้าย asyncio.timeout ของ Python โค้ดตัวอย่าง:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • เฟรมเวิร์ก async ส่วนใหญ่มักมองข้ามเรื่อง timeout และ cancellation ที่จริงแล้วนั่นแหละคือส่วนที่ยากที่สุด
  • ใน Scala ก็มีไลบรารี concurrency ชื่อ ZIO อยู่แล้ว อ้างอิง: zio.dev
  • ช่วงหลังประทับใจกับ Tokio ของ Rust มาก และถ้าใน Zig จะทำ concurrency สไตล์ Go ได้โดยไม่ต้องมี GC ก็น่าอยากลองมาก
    • Go ใช้ลูกเล่นอย่าง สแตกที่ขยายได้ไม่สิ้นสุด ได้เพราะมี GC แต่ Zig แม้จะเป็นภาษาระดับล่าง ก็ยังแสดง API ระดับสูง ได้อย่างสวยงาม ซึ่งน่าประทับใจมาก
  • ตอนที่รู้จัก Zig ครั้งแรกก็มาจากเว็บไซต์ของ Bun ทุกวันนี้มันพัฒนาเร็วมากจริงๆ
  • ในเวอร์ชัน C++ เก่า ฉันเคยทำ async I/O ด้วย Qt แต่รอบนี้เปลี่ยนมาใช้ Go แล้ว ทั้ง Zig และ Go ต่างก็มี Qt binding ใหม่แล้ว
    • Go: miqt
    • Zig: libqt6zig ฉันอยากได้ binding สำหรับ Rust cxx-qt เป็นโปรเจกต์เดียวที่ยังมีการดูแล แต่ฉันไม่อยากใช้ QML หรือ CMake อยากใช้ Qt ด้วย Rust + Cargo ล้วนๆ
  • ใน Scala ก็มีเฟรมเวิร์กชื่อดังชื่อ ZIO อยู่แล้ว เลยรู้สึกว่าการตั้งชื่อนี่ช่างยากจริงๆ