4 คะแนน โดย GN⁺ 2025-12-04 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ภาษา Zig นำเสนอ โมเดลใหม่บนพื้นฐาน Io อินเทอร์เฟซ เพื่อแก้ปัญหาความซับซ้อนของการออกแบบ I/O แบบอะซิงโครนัสเดิม
  • โมเดลนี้รักษาโครงสร้างฟังก์ชันเดียวกันสำหรับโค้ดซิงโครนัสและอะซิงโครนัส โดยมี Io.Threaded และ Io.Evented เป็นตัวดำเนินการ
  • Io.Threaded ทำงานแบบซิงโครนัสตามค่าเริ่มต้น ส่วน Io.Evented รันแบบอะซิงโครนัสด้วย event loop
  • นักพัฒนาสามารถควบคุมการทำงานแบบขนานผ่านฟังก์ชัน async() และ concurrent() และปรับประสิทธิภาพได้โดยไม่ต้องแก้โค้ด
  • แนวทางนี้แก้ปัญหา การระบายสีฟังก์ชัน (function coloring) และคงความเรียบง่ายและการควบคุมของ Zig ไว้ ขณะเดียวกันยังคงประสิทธิภาพอะซิงโครนัสได้

การเปลี่ยนแปลงการออกแบบอะซิงโครนัสของ Zig

  • Zig ค้นหาวิธีใหม่เนื่องจากการออกแบบอะซิงโครนัสเดิมไม่สอดคล้องกับปรัชญาความมินิมัลลิสม์ของภาษา
    • การออกแบบเดิมมีการผสานรวมกับฟังก์ชันอื่นไม่สูง
    • โมเดลใหม่สามารถจัดการ I/O แบบซิงโครนัสและอะซิงโครนัสด้วยโครงสร้างโค้ดเดียวกัน
  • โมเดลใหม่ทำงานรอบ อินเทอร์เฟซ generic Io
    • ฟังก์ชัน I/O ทั้งหมดรับพารามิเตอร์เป็นอินสแตนซ์ Io
    • คล้ายโครงสร้างอินเทอร์เฟซ Allocator ทำให้ควบคุม I/O ได้แบบเดียวกับการจัดสรรหน่วยความจำ

โครงสร้างของอินเทอร์เฟซ Io

  • ไลบรารีมาตรฐานมี ตัวใช้งานมาตรฐาน 2 แบบ
    • Io.Threaded: รันแบบซิงโครนัสเป็นค่าเริ่มต้น ใช้งานเธรดแบบขนานได้เมื่อจำเป็น
    • Io.Evented: รันแบบอะซิงโครนัสบน event loop (ใช้ io_uring, kqueue และอื่น ๆ)
  • ผู้ใช้สามารถเขียน ตัวใช้งาน Io ใหม่เอง เพื่อให้ควบคุมวิธีการรันได้ละเอียดขึ้น

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

  • ฟังก์ชันตัวอย่าง saveFile() ทำหน้าที่สร้างไฟล์ เขียน และปิดไฟล์
    • เมื่อใช้ Io.Threaded จะทำงานผ่าน system call ตามปกติ
    • เมื่อใช้ Io.Evented จะทำงานผ่าน backend แบบอะซิงโครนัส
    • ในทั้งสองกรณี การเรียก writeAll() จะรับประกันว่าเสร็จสิ้นงาน
  • โค้ดเดียวกันทำงานได้ทั้งในสภาพแวดล้อมซิงโครนัสและอะซิงโครนัสเหมือนกัน
    • ผู้เขียนไลบรารีไม่จำเป็นต้องกังวลเรื่องวิธีการรัน

การรันแบบขนานและ async() / concurrent()

  • ฟังก์ชัน async() เป็นการร้องขอให้รันแบบอะซิงโครนัส แต่กับ Io.Threaded อาจรันได้ทันที
    • ใน Io.Evented จะรันแบบอะซิงโครนัสจริง และสามารถบันทึกไฟล์พร้อมกันได้สองไฟล์
  • ฟังก์ชัน concurrent() ใช้เมื่อจำเป็นต้องรันแบบขนานจริง
    • Io.Threaded ใช้ thread pool
    • Io.Evented จัดการเหมือนกับ async()
  • การเลือกฟังก์ชันที่ผิด (async แทน concurrent) จะถือว่าเป็นบั๊ก และไม่สามารถป้องกันได้ในระดับภาษา

รูปแบบโค้ดและการผสานรวมกับภาษา

  • รักษาสไตล์โค้ด Zig ทั่วไปโดยไม่มีกฎไวยากรณ์เฉพาะสำหรับอะซิงโครนัส
    • ใช้ try, defer และโครงสร้างควบคุมกระแสเดิมต่อไป
    • Andrew Kelley กล่าวว่าสามารถอ่านได้เหมือน โค้ด Zig มาตรฐาน
  • มีตัวอย่างการนำเสนอ การ lookup DNS แบบอะซิงโครนัส
    • แตกต่างจาก getaddrinfo() ในแง่ที่คืนผลลัพธ์ความสำเร็จแรกเท่านั้นและยกเลิกคำขอที่เหลือ

แผนงานและสถานะการพัฒนา

  • Io.Evented ยังอยู่ในขั้นทดลอง และยังไม่รองรับบางระบบปฏิบัติการ
  • การพัฒนา Io สำหรับ WebAssembly อยู่ระหว่างแผนงาน และยังมีความจำเป็นต้องพัฒนาฟีเจอร์ที่เกี่ยวข้อง
  • มี 24 งานย่อยหลังการปรับปรุง Io และส่วนใหญ่ยังไม่แล้วเสร็จ
  • Zig ยังไม่ถึงรุ่น 1.0 การทำ I/O แบบอะซิงโครนัสและการสร้างโค้ดเนทีฟยังเป็นงานสำคัญที่เหลืออยู่
  • ด้วยโมเดลใหม่นี้คาดว่าความถี่การเขียนโค้ดใหม่เพราะการเปลี่ยนอินเทอร์เฟซ I/O จะลดลง

สรุปจากการอภิปรายในชุมชน

  • ความคิดเห็นหลายส่วนประเมินว่าแนวทางของ Zig ง่ายและยืดหยุ่นกว่ารูปแบบ async/await ของ Rust
    • Rust มีความซับซ้อนมากขึ้นเมื่อมี executor หลายตัวทำงานร่วมกัน
    • Zig ทำให้ความเป็นไปได้ของการอยู่ร่วมกันของ executor หลายตัวผ่านอินเทอร์เฟซ Io เป็นไปได้
  • บางส่วนชี้ว่าโค้ดอาจยืดยาวไปบ้าง
    • อย่างไรก็ตาม API ที่ชัดเจนช่วยเพิ่มการควบคุมด้านความปลอดภัย ประสิทธิภาพ และการทดสอบ
  • การอภิปรายทางเทคนิครวมถึงความต่างระหว่างการรันด้วย thread กับ async, รวมถึงการใช้งาน stackful และ stackless coroutine
  • Io ของ Zig ทำเป็นการขยายไลบรารีมาตรฐานโดยไม่ต้องมีการจัดการพิเศษระดับภาษา
    • คาดว่าจะเพิ่มความสามารถแบบ stackless coroutine ในอนาคต

สรุป

  • โมเดลอะซิงโครนัสใหม่ของ Zig มีเป้าหมายให้ความเรียบง่ายของภาษาและประสิทธิภาพ I/O สูงสุดอยู่ร่วมกัน
  • การแก้ปัญหา function coloring, การผสานโค้ดซิงโครนัสและอะซิงโครนัส, และโครงสร้างควบคุมแบบชัดเจน ทำให้โมเดลนี้ถูกประเมินว่าเป็นขั้นตอนสำคัญสู่ความเสถียรของ Zig 1.0

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

 
GN⁺ 2025-12-04
ความคิดเห็นจาก Hacker News
  • โดยรวมแล้วบทความนี้ถูกต้องและค้นคว้ามาอย่างดี
    แต่มี จุดแก้ไขเล็กน้อย อยู่บางประการ
    ในอินสแตนซ์ Io.Threaded นั้น async() ไม่ได้ทำงานแบบอะซิงโครนัสจริง ๆ แต่จะรันทันที อย่างไรก็ตาม std.Io.Threaded จะใช้ thread pool เป็นค่าเริ่มต้นเพื่อกระจายงานอะซิงโครนัส
    แต่ถ้าเริ่มต้นด้วย init_single_threaded ก็จะทำงานแบบที่อธิบายไว้ในบทความ
    อีกอย่างหนึ่งคือ แต่ก่อนมีฟังก์ชันชื่อ asyncConcurrent() แต่ตอนนี้เปลี่ยนชื่อเป็น concurrent() เฉย ๆ แล้ว

    • ผมคือ Daroc ผมได้ปรับใช้ การแก้ไขสองจุด ในบทความตามฟีดแบ็กนี้แล้ว
      ถ้าต้องการส่งฟีดแบ็กในอนาคต สามารถส่งอีเมลไปที่ lwn@lwn.net ได้
      ขอบคุณสำหรับข้อเสนอการแก้ไขและงานที่เกี่ยวข้องกับ Zig
    • มีคำถามถึง Andrew
      ผมสงสัยว่าถ้าใช้ asyncConcurrent() ผิดในจุดที่ควรใช้ async() จะทำให้เกิด บั๊ก แบบไหน
      อยากรู้ว่าขึ้นอยู่กับโมเดล IO แล้วอาจกลายเป็น UB (undefined behavior) ได้ไหม หรือเป็นแค่ข้อผิดพลาดทางตรรกะ
    • ข้อดีของ concurrent() คือช่วยเพิ่ม ความอ่านง่ายและพลังในการสื่อความหมาย ของโค้ด ทำให้เห็นชัดว่า “โค้ดนี้ต้องรันแบบขนานเท่านั้น”
  • ผมคิดว่าดีไซน์นี้ค่อนข้างสมเหตุสมผล
    แต่คำอธิบายของ Zig ทำให้งง
    มันชอบย้ำว่าแก้ปัญหา function coloring ได้แล้ว แต่จริง ๆ แล้วก็แค่ยัด IO เข้าไปเป็น effect type เท่านั้น
    มันเป็นรูปแบบที่ผู้เรียกต้องเก็บ token ไว้ ดังนั้นก็ยังเป็นการระบายสีอยู่ดี
    ผมมองว่าคล้ายกับแนวทาง async ของ Go

    • ถ้าแค่เรียกด้วยอาร์กิวเมนต์ต่างกันก็ถือเป็น ‘ฟังก์ชันที่ถูกระบายสี’ งั้นทุกฟังก์ชันก็ถูกระบายสีหมดและคำนี้ก็หมดความหมาย ;)
      โมเดล async-await แบบเก่าของ Zig ก็แก้ปัญหา coloring ได้อยู่แล้ว
      เพราะคอมไพเลอร์จะสร้าง เวอร์ชัน sync/async ให้อัตโนมัติตามบริบทของการเรียก
    • จริง ๆ แล้วหัวใจของ function coloring คือ การทำซ้ำของเส้นทางโค้ด sync/async
      Zig แก้ปัญหานี้ด้วย dependency injection ซึ่งในทางปฏิบัติก็เพียงพอแล้ว
      ความซับซ้อนของการเรียก async เป็นสิ่งที่เลี่ยงไม่ได้ แต่ก็เป็นราคาที่ต้องจ่ายหากต้องการการควบคุมที่ละเอียด
    • io ของ Zig ไม่ใช่ effect type แบบแพร่กระจาย
      คุณสามารถประกาศตัวแปร io แบบ global แล้วใช้จากที่ไหนก็ได้ (แน่นอนว่าไม่แนะนำเวลาทำไลบรารี)
      ถ้าดูบทความ What color is your function? ที่สรุปเงื่อนไขห้าข้อของปัญหา function coloring จะเห็นว่าแนวทางของ Zig น่าจะไม่เข้าเงื่อนไขบางข้อ (โดยเฉพาะข้อ 4 และ 5)
    • โดยพฤตินัยแล้ว Zig เหมือน ระบายสีทุกอย่างเป็น async และให้เลือกแค่ว่าจะใช้ worker thread หรือไม่
      แต่แนวทางแบบนี้อาจก่อปัญหาอย่าง deadlock ได้
      บางส่วนของโค้ดไม่ปลอดภัยต่อการใช้งานข้ามเธรด ดังนั้นการมี coloring กลับช่วยได้
    • ในมุมของนักพัฒนา Haskell มันดูเหมือน Zig กำลังทำ IO monad โดยไม่มีการรองรับจากตัวภาษา
  • ดีไซน์นี้ดูคล้าย async ของ Scala มาก
    ใน Scala จะส่ง execution context ผ่าน implicit parameter ส่วน Zig รับมาแบบ explicit
    ในทางปฏิบัติ มันไม่ได้ดีกว่าการใช้ thread กับ queue ตรง ๆ มากนัก และการจัดการ execution context ก็ทำให้เกิด ความซับซ้อนและพฤติกรรมที่คาดเดาได้ยาก
    ดูเหมือนทีม Zig จะมีประสบการณ์กับ Scala ไม่มากนักเลยคิดว่าแนวทางนี้ใหม่

    • ถ้าใช้ OS thread โดยตรง จะชนข้อจำกัดด้านการขยายระบบตาม กฎของ Little
      JVM แก้เรื่องนี้ด้วย virtual thread แต่ภาษา low-level จะทำประสิทธิภาพแบบเดียวกันได้ยาก
      ดังนั้นภาษาตระกูล Zig จึงต้องการวิธีแก้ปัญหาด้าน scalability แบบอื่น
    • อ้างอิง ExecutionContext API ของ Scala เพื่อทำความเข้าใจแนวคิดที่เกี่ยวข้องได้ดีขึ้น
  • ในระบบ async/await แบบเก่าของ Zig นั้นสามารถ suspend/resume ฟังก์ชันได้
    ผมเคยอยากใช้ฟีเจอร์นี้ตอนพัฒนา OS เพื่อทำ การพัก/กลับมาทำงานต่อของเฟรม ที่อิงกับ device interrupt
    น่าเสียดายที่ระบบ io ใหม่ดูเหมือนจะต้องทำสิ่งนี้เองโดยตรง

    • มี low-level builtin ชื่อ @asyncSuspend และ @asyncResume
      Io แบบใหม่เป็น abstraction กลางสำหรับ synchronous, threaded และ event-based ดังนั้นจึงไม่ได้รวมกลไก suspend ไว้
    • ท้ายที่สุดแล้ว suspend/resume อาจถูกทำเป็น ฟังก์ชันใน standard library ฝั่ง user space
      ถ้าดู Io.Evented prototype ปัจจุบัน ก็มีความเป็นไปได้ว่าจะจัดการผ่านไลบรารีภายนอกโดยอิง stackless coroutine
    • ผมก็สงสัยเหมือนกันว่าจะทำ suspend/resume โดยมี thread pool แค่ชุดเดียวได้หรือไม่
    • และก็ไม่แน่ใจว่าการทำ cooperative coroutine ให้เป็น preemptive async จะมีความหมายอย่างไร
  • ในโค้ดตัวอย่างมีการบอกว่าเมื่อ writeAll() คืนค่ากลับมา งานก็ถือว่าเสร็จแล้ว
    แต่เนื่องจาก implementation ของ IO อาจหลากหลาย จริง ๆ แล้วควรรับประกันว่าเสร็จสิ้นตอน เริ่ม defer
    ไม่อย่างนั้นก็ต้องมี การติดตาม dependency ระหว่าง createFile กับ writeAll
    ซึ่งถ้าเป็นแบบนั้นก็ดูไม่ต่างจาก blocking call เท่าไร
    และชื่ออินเทอร์เฟซว่า IO ก็ยังดูไม่ชัดเจนว่าทำไมถึงใช้ชื่อนี้
    จริง ๆ แล้วมันใกล้เคียงกับ abstraction สำหรับ “ไปรันใน context อื่น” มากกว่า
    เอกสารที่เกี่ยวข้อง: std.Io

  • ตัวอย่างต่อไปนี้น่าสนใจ

    var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
    var b_future = io.async(saveFile, .{io, data, "saveB.txt"});
    const a_result = a_future.await(io);
    const b_result = b_future.await(io);
    

    ใน Rust หรือ Python ถ้า coroutine ไม่ได้ถูก await มันก็จะไม่เดินหน้าต่อ
    แต่ในตัวอย่างของ Zig ถ้า io.async เดินหน้าเองได้ แบบนั้นมันก็คล้ายกับ การสร้าง task
    นี่เป็นดีไซน์ที่ใช้ได้ แต่ไม่ใช่ทิศทางที่ภาษาอื่นส่วนใหญ่เลือก

    • C# ก็ทำงานคล้ายกัน โดยฟังก์ชัน async จะ รันบนเธรดที่เรียกก่อนจนกว่าจะถึงจุด yield
    • ใน Zig ก็เช่นกัน ต้องเรียก .await(io) ก่อนจึงจะรับประกันการทำงาน
      จะรันทันทีหรือถูกคิวลง thread pool ขึ้นอยู่กับ implementation ของ Io runtime
    • ในทางปฏิบัติ การทำงานจะเดินหน้าตอน await
      สำหรับ evented io งานสองชิ้นอาจรันแบบ สลับกัน (interleaved) ได้ และใน threaded io ก็อาจทำงานอยู่เบื้องหลังได้
      กล่าวคือไม่มี “task ที่แอบไปรันเองอยู่ที่ไหนสักแห่ง”
    • JavaScript ก็ทำงานแนวนี้เช่นกัน
  • ในฐานะคนที่ใช้ Go ทุกวัน ผมรู้สึกว่า Io ของ Zig แก้ ข้อด้อยหลายอย่าง ของ Go ได้
    แต่อยากรู้ว่า Zig มีแนวคิดเรื่อง channel หรือไม่
    ใน Go มีคีย์เวิร์ด select แต่ใช้กับ socket ไม่ได้ ซึ่งผมรู้สึกติดใจเรื่องนี้มาตลอด

    • มีคนชี้ว่า ถ้าห่อ IO ทุกอย่างด้วย channel จะ มีต้นทุนสูง
      channel ของ Go มี overhead ระดับหลายสิบ cycle ดังนั้นกับ IO ชิ้นเล็ก ๆ จะไม่คุ้ม
      แต่สำหรับ การย้ายข้อมูลก้อนใหญ่ หรือ การซิงก์แบบหลายต่อหลาย มันก็ยังมีประโยชน์
    • ใน Zig มี std.Io.Queue ที่คล้าย channel ของ Go
      จะทำ select แบบคล้ายกันก็ได้ แต่ในเชิงไวยากรณ์จะ ไม่ ergonomic เท่า
      ข้อดีคือมันทำงานได้บน IO runtime ที่หลากหลายโดยไม่ต้องมี GC
    • อยากถามว่าเคยใช้ภาษา Odin ไหม มันเป็น “better C” ที่ได้แรงบันดาลใจจาก Go มากกว่า Zig
    • ผมชอบที่มันไม่ได้บังคับให้มี colored function แบบ async/await ของ C#
      ผมคิดว่าแนวทาง “colorless” ของ Zig ดีกว่ามาก
    • การคิดว่า concurrency model ของ Go เป็นของพิเศษนั้นเป็นปัญหา
      Goroutine ก็เป็นแค่ green thread และ channel ก็เป็นแค่ queue ที่ thread-safe เท่านั้น และ Zig ก็มีสิ่งเหล่านี้ใน standard library อยู่แล้ว
  • Io เวอร์ชัน async ของ Zig ดูแทบจะเหมือนแนวทางของ Go ทุกอย่าง
    แต่ใน Go เวลาเรียก C library จะมี ต้นทุนการจัดสรรสแตก สูง และการทำ syscall โดยตรง ก็มีปัญหาเรื่องความเข้ากันได้ข้ามแพลตฟอร์ม
    Zig ดูเหมือนจะทำให้สิ่งเหล่านี้ ประกอบเลือกใช้ได้ เพื่อให้เลือก trade-off ได้หลายแบบโดยไม่ต้องแก้โค้ด

  • async IO แบบใหม่ยอดเยี่ยมมากในตัวอย่างง่าย ๆ แต่กับ IO ที่ซับซ้อนระดับเซิร์ฟเวอร์ อาจมีข้อจำกัด
    ผมได้เปิด issue ที่เกี่ยวข้องไว้ใน GitHub แล้ว

  • ประเด็นหลักคือผู้ออกแบบภาษาและไลบรารีควรมีวิธีเชื่อม execution context ที่ต่างกัน (sync/async) เข้าหากัน
    วิธีหนึ่งคือห่อ context ด้วย FSM (finite-state machine) และมี ช่องทางสื่อสาร ระหว่างทั้งสองฝั่ง
    บทความที่เกี่ยวข้อง: Function colors represent different execution contexts