1 คะแนน โดย GN⁺ 2026-05-06 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • Async Rust ช่วยให้โค้ดที่ไม่ขึ้นกับ executor สามารถรันได้ทั้งบนเซิร์ฟเวอร์และไมโครคอนโทรลเลอร์ แต่ state machine ที่คอมไพเลอร์สร้างขึ้นทำให้ขนาดไบนารีเพิ่มขึ้นอย่างชัดเจน โดยเฉพาะในงาน embedded
  • แม้แต่ตัวอย่างง่าย ๆ อย่าง bar() ที่มีจุด await 2 จุด ก็ยังสร้าง MIR 360 บรรทัด และสถานะ Unresumed, Returned, Panicked, Suspend0, Suspend1 ขณะที่เวอร์ชัน synchronous ต้องใช้เพียง 23 บรรทัด
  • หากเปลี่ยนให้ future ที่เสร็จแล้วเมื่อถูก poll ซ้ำคืนค่า Poll::Pending แทน panic ก็ยังคงเป็นไปตามสัญญาโดยไม่เกิด unsafe behavior และจากการทดลองพบว่า ขนาดไบนารีของ embedded firmware ลดลง 2%~5%
  • แม้ async { 5 } ที่ไม่มี await ปัจจุบันก็ยังสร้าง state machine ที่มี 3 สถานะพื้นฐาน แต่หากปรับให้คืน Poll::Ready(5) ทุกครั้งแทน จะทำให้ขนาดไบนารี embedded ลดลง 0.2%
  • Project Goal ที่เสนอขึ้นมามุ่งผลักดันงานในคอมไพเลอร์ เช่น ตัด panic หลังเสร็จสิ้นในโหมด release, ตัด state machine ของ async block ที่ไม่มี await, inline future ที่มี await เดียว และรวมสถานะที่ซ้ำกัน

ปัญหาความเทอะทะระดับคอมไพเลอร์ของ Async Rust

  • Async Rust ช่วยให้โค้ดที่ไม่ขึ้นกับ executor สามารถทำงานได้พร้อมกันทั้งบนเซิร์ฟเวอร์และไมโครคอนโทรลเลอร์ แต่บนไมโครคอนโทรลเลอร์ขนาดเล็ก การเพิ่มขึ้นของขนาดไบนารีจะเห็นได้ชัดเป็นพิเศษ
  • บล็อก Rust เคยแนะนำ async/await ว่าเป็น zero-cost abstraction แต่ในทางปฏิบัติ async กลับสร้างความเทอะทะ (bloat) จำนวนมาก และแม้เดสก์ท็อปกับเซิร์ฟเวอร์ก็มีปัญหาเดียวกัน เพียงแต่มีหน่วยความจำและทรัพยากรประมวลผลมากพอจนมองเห็นได้น้อยกว่า
  • ต่อจาก วิธีเลี่ยง เพื่อลดความเทอะทะเวลาที่เขียนโค้ด async จึงมีการยื่น Project Goal เพื่อแก้ปัญหานี้ในคอมไพเลอร์
  • ปัญหาที่ future มีขนาดใหญ่เกินจำเป็นและมีการคัดลอกมากเกินไปไม่ได้อยู่ในขอบเขตนี้
    • ปัญหานี้เป็นที่รู้จักอยู่แล้ว และมี PR ที่เปิดไว้เพื่อจัดการบางส่วน: https://github.com/rust-lang/rust/pull/135527

โครงสร้างของ future ที่ถูกสร้างขึ้น

  • โค้ดตัวอย่างคือ foo() คืนค่า async { 5 } และ bar() ทำ foo().await + foo().await
    • ตัวอย่าง Godbolt: godbolt
  • bar มีจุด await 2 จุด ดังนั้น state machine จึงต้องมีอย่างน้อย 2 สถานะ แต่ในความเป็นจริงกลับมีสถานะมากกว่านั้น
  • Rust compiler สามารถ dump MIR ได้หลาย pass และ pass coroutine_resume คือ MIR pass สุดท้ายที่เฉพาะกับ async
    • async ยังคงอยู่ใน MIR แต่ไม่เหลืออยู่ใน LLVM IR แล้ว ดังนั้นกระบวนการแปลง async เป็น state machine จึงเกิดขึ้นใน MIR pass
  • ฟังก์ชัน bar สร้าง MIR 360 บรรทัด ขณะที่เวอร์ชัน synchronous ใช้เพียง 23 บรรทัด
  • CoroutineLayout ที่คอมไพเลอร์แสดงออกมานั้นโดยพื้นฐานคือชุดสถานะแบบ enum
    • Unresumed: สถานะเริ่มต้น
    • Returned: สถานะที่เสร็จสิ้นแล้ว
    • Panicked: สถานะหลัง panic
    • Suspend0: จุด await แรก และเก็บ future ของ foo
    • Suspend1: จุด await ที่สอง และเก็บผลลัพธ์แรกพร้อม future ของ foo ตัวที่สอง
  • Future::poll เป็นฟังก์ชันที่ปลอดภัย ดังนั้นแม้จะถูกเรียกอีกครั้งหลัง future เสร็จแล้ว ก็ต้องไม่ก่อให้เกิด UB
    • ปัจจุบัน หลัง Suspend1 จะคืน Ready และเปลี่ยน future ไปเป็นสถานะ Returned
    • หาก poll อีกครั้งในสถานะนี้ จะเกิด panic
  • สถานะ Panicked ดูเหมือนมีไว้เพื่อป้องกันไม่ให้ future ถูก poll อีกครั้ง หลัง async function panic แล้วถูกจับด้วย catch_unwind
    • เพราะหลัง panic แล้ว future อาจอยู่ในสถานะที่ไม่สมบูรณ์ และการ poll ซ้ำอาจนำไปสู่ UB
    • กลไกนี้คล้ายกับ mutex poisoning มาก
    • การตีความสถานะ Panicked แบบนี้ยังไม่มีเอกสารยืนยันชัดเจน จึงมั่นใจประมาณ 90%

การ poll หลังเสร็จสิ้นจำเป็นต้อง panic หรือไม่

  • future ในสถานะ Returned ปัจจุบันจะ panic แต่ไม่ได้จำเป็นว่าต้องเป็นแบบนั้นเสมอไป
    • เงื่อนไขที่จำเป็นจริง ๆ คือห้ามก่อให้เกิด UB
  • panic มีต้นทุนค่อนข้างสูง และเพิ่มเส้นทางที่มีผลข้างเคียงซึ่งยากต่อการลบออกด้วยการ optimize
  • หาก future ที่เสร็จแล้วเมื่อถูก poll ซ้ำคืน Poll::Pending ก็จะยังคงเป็นไปตามสัญญาของชนิด Future โดยไม่เกิด unsafe behavior
  • เมื่อแก้คอมไพเลอร์เพื่อทดลองแนวทางนี้ พบว่าใน embedded firmware ที่ใช้ async ขนาดไบนารีลดลง 2%~5%
  • มีข้อเสนอให้เปิดพฤติกรรมนี้เป็นสวิตช์คล้าย overflow-checks = false ของ integer overflow
    • ใน debug build ยังคง panic ต่อไปเพื่อให้พฤติกรรมที่ผิดถูกเปิดเผยทันที
    • ใน release build จะได้ future ที่มีขนาดเล็กลง
  • เมื่อใช้ panic=abort อาจมีโอกาสตัดสถานะ Panicked ออกไปได้เลย แต่ผลกระทบยังต้องพิจารณาเพิ่มเติม

ต่อให้ไม่มี await ก็ยังสร้าง state machine เสมอ

  • foo() คืนค่าเพียง async { 5 } ดังนั้นรูปแบบที่เหมาะที่สุดหากเขียนเองคือ future ที่ไม่มีสถานะ และคืน Poll::Ready(5) ทุกครั้ง
  • แต่ MIR ที่คอมไพเลอร์สร้างขึ้นยังคงมี 3 สถานะพื้นฐานคือ Unresumed, Returned, Panicked
    • ตอน poll จะตรวจ discriminant ของสถานะปัจจุบันแล้วจึงแตกแขนง
    • หาก poll อีกครั้งหลังเสร็จ จะ panic ด้วย assert `async fn` resumed after completion
  • ในกรณีนี้สามารถ optimize ได้โดยไม่สร้าง state machine และคืน Poll::Ready(5) ทุกครั้งแทน
  • เมื่อลองใส่การเปลี่ยนแปลงนี้ในคอมไพเลอร์แบบทดลอง พบว่าขนาดไบนารี embedded ลดลง 0.2%
    • แม้การลดลงจะไม่มาก แต่เป็น optimization ที่เรียบง่าย จึงอาจคุ้มค่าที่จะนำมาใช้
  • optimization นี้เปลี่ยนพฤติกรรมเล็กน้อย แต่ผู้ที่ได้รับผลกระทบมีเพียง executor ที่ไม่ทำตามข้อกำหนดเท่านั้น
    • ปัจจุบันคอมไพเลอร์จะ panic เมื่อมีการ poll ภายหลัง
    • หลัง optimize แล้ว future จะคืน Ready เสมอ

พึ่งพา LLVM อย่างเดียวไม่เพียงพอ

  • แม้ผลลัพธ์ MIR จะไม่มีประสิทธิภาพ แต่บางครั้ง LLVM ก็สามารถจัดการให้ได้ทั้งหมด อย่างไรก็ดีเงื่อนไขค่อนข้างจำกัด
    • future ต้องเรียบง่ายพอ
    • ต้องใช้ opt-level=3
  • เมื่อ future ซับซ้อนขึ้น LLVM จะลบส่วนเกินออกไม่ได้ และเพราะโค้ด async Rust ตามแบบที่ใช้กันทั่วไปมักมี future ซ้อนลึก ความซับซ้อนจึงเพิ่มขึ้นอย่างรวดเร็ว
  • ในสภาพแวดล้อมที่มัก optimize เพื่อขนาดอย่าง embedded หรือ wasm, LLVM ไม่สามารถ optimize ทุกอย่างได้หมด
  • ตัวอย่าง Godbolt: https://godbolt.org/z/58ahb3nne
    • ใน assembly ที่สร้างขึ้น LLVM รู้ว่า foo คืนค่า 5 แต่ยัง optimize คำตอบของ bar ให้เป็น 10 ไม่ได้
    • การเรียก poll ของ foo ก็ยังคงอยู่
    • สาเหตุคือยังมีเส้นทาง panic ที่เป็นไปได้ซึ่งคอมไพเลอร์ไม่สามารถสรุปทิ้งได้ทั้งหมด
    • LLVM ไม่รู้ว่าแท้จริงแล้ว foo ถูกเรียกเพียงครั้งเดียวและไม่ panic
  • หากคอมเมนต์ทิ้ง branch ที่ panic ใน IR จะ optimize ได้ดีขึ้น: https://godbolt.org/z/38KqjsY8E
  • แทนที่จะหวังการ optimize ภายหลังจาก LLVM คอมไพเลอร์ควรส่งอินพุตที่ดีกว่าให้ LLVM ตั้งแต่แรก

การ inline future ยังทำได้ไม่ดี

  • การ inline สำคัญเพราะเปิดทางให้ optimization pass อื่น ๆ ทำงานต่อได้ แต่ future ที่ Rust สร้างขึ้นในปัจจุบันยังไม่ถูก inline ในช่วงต้น
  • หลังแต่ละ future ได้ implementation แล้ว LLVM และ linker จึงค่อยมีโอกาส inline แต่จากปัญหาก่อนหน้านี้ เวลานั้นถือว่าช้าเกินไป
  • โอกาส inline ที่ตรงไปตรงมาที่สุดคือรูปแบบที่ bar() ทำเพียง foo(blah).await
    • เป็นแพตเทิร์นที่พบได้บ่อยเมื่อสร้าง abstraction ด้วย trait
    • ปัจจุบันคอมไพเลอร์จะสร้าง state machine สำหรับ bar แล้วค่อยเรียก state machine ของ foo ข้างใน
    • ที่มีประสิทธิภาพกว่าคือให้ bar กลายเป็น future ของ foo เอง
  • กรณีที่มี preamble และ postamble จะซับซ้อนขึ้น
    • เช่น bar(input) สร้าง blah จาก input > 10 จากนั้น foo(blah).await แล้วนำผลลัพธ์ไป * 2
    • พบได้บ่อยเวลาที่แปลง async function ไปเป็น signature อื่น โดยเฉพาะใน trait implementation
  • bar ในรูปแบบนี้ก็ยังไม่จำเป็นต้องมีสถานะ async ของตัวเอง
    • เพราะไม่มีข้อมูลใดที่ต้องถูกเก็บข้ามจุด await เดียว นอกจากค่าที่ถูกจับไว้ใน foo
    • เพียงแต่ bar จะกลายเป็น foo ตรง ๆ ไม่ได้ และต้องพึ่งพาสถานะส่วนใหญ่จาก foo
  • หากเขียนเอง BarFut อาจมีสถานะ Unresumed { input } และ Inlined { foo: FooFut }
    • ในการ poll ครั้งแรก จะรัน preamble เพื่อสร้าง foo(blah) แล้วเปลี่ยนเป็นสถานะ Inlined
    • หลังจากนั้นจะนำผลลัพธ์ของ foo.poll(cx) ไปผ่าน postamble
  • หากสามารถรันโค้ดล่วงหน้าก่อนถึง await จุดแรกได้ ก็อาจตัดสถานะ Unresumed ออกได้ด้วย แต่มีการรับประกันว่า future จะไม่ทำอะไรเลยก่อนถูก poll จึงเปลี่ยนไม่ได้
  • หากสามารถ query คุณสมบัติของ future ที่กำลังถูก poll ได้ ก็จะเปิดทางให้มี optimization การ inline เพิ่มเติม
    • ตัวอย่างเช่น หากรู้ว่า future คืนค่า ready เสมอในการ poll ครั้งแรก ก็ไม่จำเป็นที่ future ฝั่งผู้เรียกจะต้องสร้างสถานะสำหรับ await จุดนั้น
    • หากนำ optimization แบบนี้ไปใช้แบบ recursive ก็อาจยุบ future จำนวนมากให้กลายเป็น state machine ที่เรียบง่ายกว่ามากได้
  • จากโครงสร้างของ rustc ในปัจจุบัน ดูเหมือนว่า async block แต่ละก้อนจะถูกแปลงแยกกัน และข้อมูลที่เกี่ยวข้องจะไม่ถูกเก็บต่อไว้ จึงไม่สามารถ query แบบนี้ได้
  • future inline ยังไม่ได้ถูกทดลองจริง แต่คาดว่าจะช่วยได้มากทั้งกับขนาดไบนารีและประสิทธิภาพ

การรวมสถานะที่เหมือนกัน

  • สำหรับแต่ละจุด await ใน async block state machine จะมีสถานะเพิ่มขึ้น
  • โค้ดลักษณะต่อไปนี้เขียนได้เป็นธรรมชาติ แต่เพราะ await async function เดียวกันในสอง branch จึงเกิดสถานะที่เหมือนกัน 2 ชุด
    • CommandId::A => send_response(123).await
    • CommandId::B => send_response(456).await
  • ในกรณีนี้ CoroutineLayout จะมี _s0, _s1 ซึ่งเก็บ coroutine type เดียวกันของ send_response และมีสถานะ Suspend0, Suspend1 สองสถานะ
  • MIR ของฟังก์ชันนี้มีความยาว 456 บรรทัด และหลาย basic block ก็แทบจะซ้ำกันทั้งหมด
  • หาก refactor เองโดยคำนวณเฉพาะค่าตอบกลับก่อน แล้วค่อยเรียก send_response(response).await เพียงครั้งเดียว สถานะซ้ำเหล่านี้จะหายไป
    • CommandId::A คือ 123
    • CommandId::B คือ 456
    • จากนั้น send_response(response).await
  • หลัง refactor แล้ว CoroutineLayout จะเหลือ future ที่เก็บไว้เพียงตัวเดียว และเหลือแค่สถานะ Suspend0 สถานะเดียว
  • ความยาว MIR ทั้งหมดลดลงเหลือ 302 บรรทัด และความซ้ำซ้อนหายไป
  • ดังนั้น optimization pass ที่ค้นหาเส้นทางโค้ดและสถานะที่เหมือนกันแล้วรวมเข้าด้วยกันจึงดูมีประโยชน์
    • optimization นี้มีแนวโน้มว่าจะทำงานร่วมกับ future inline pass ได้ดี

ลิงก์การทดลองและ benchmark เพิ่มเติม

ขอการสนับสนุน Project Goal

  • งานนี้ถูกยื่นเป็น Project Goal เพื่อดำเนินการในคอมไพเลอร์: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
  • หากไม่มีเงินทุนก็ยากที่จะเดินหน้างานได้มาก จึงต้องการการสนับสนุนบางส่วนหรือทั้งหมดจากบริษัทหรือองค์กรที่จะได้รับประโยชน์จากงานนี้
  • ติดต่อได้ที่ dion@tweedegolf.com
  • ขอบเขตงานและขนาดเงินทุนที่ต้องการยังยืดหยุ่นได้ แต่คาดว่า €30k น่าจะเพียงพอสำหรับทำงานทั้งหมดหรืออย่างน้อยส่วนใหญ่ให้เสร็จ

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

 
GN⁺ 2026-05-06
ความคิดเห็นจาก Hacker News
  • เห็นด้วยว่าพาดหัวค่อนข้างเวอร์ไปหน่อย แต่ตัวบทความเขียนดีและสื่อประเด็นได้ดี
    ผมยังมีประสบการณ์กับ Rust async ไม่มากพอจะออกความเห็นแรง ๆ แต่มีบางอย่างที่สะดุดตา
    ข้อดีคือสามารถมี runtime แบบ explicit ได้ แทนที่จะทำให้ทั้งโปรเจ็กต์ปนเปื้อนเป็น async ไปหมด ก็ปล่อยให้ค่าเริ่มต้นเป็น synchronous แล้วใช้ runtime เฉพาะที่ “ขอบเขต” ของ I/O เท่านั้น
    สำหรับโปรเจ็กต์ที่กำลังทำ วิธีนี้เข้ากันได้ดี และดูค่อนข้างคล้ายกับแนวทางที่ Zig ใช้กับโค้ด I/O ด้วย ในกรณีนี้ปัญหา function coloring ก็แทบหายไป และเพราะต้องแยก I/O ออกจากโค้ดที่เน้น CPU อย่างเคร่งครัดอยู่แล้ว การมี I/O runtime แบบ explicit จึงดูเป็นธรรมชาติ
    ข้อเสียคือทั้ง ecosystem ดูเหมือนจะพึ่งพา tokio มากเกินไป คล้ายกับกรณีที่ Java มี GC เป็นตัวเลือก แต่ในทางปฏิบัติทุกคนกลับใช้ third-party GC runtime ตัวเดียวกัน และไม่ว่าจะดึงไลบรารีอะไรมาก็โดนบังคับให้ใช้ runtime นั้น การพึ่งพาแบบรวมศูนย์เช่นนี้ไม่ค่อยดีต่อสุขภาพของ ecosystem

    • แล้วแต่บริบทว่ามันจะดูเหมือนทั้ง ecosystem พึ่ง tokio หรือไม่ แต่ถ้าดู embedded Rust จะเข้าใจมากขึ้น
      ความต้องการของ async runtime บนโปรเซสเซอร์เวิร์กสเตชันกับในสภาพแวดล้อมอย่าง RP2040 ต่างกันมาก ถึงอย่างนั้นเพราะเปลี่ยน backend ได้ เวลาเขียนโค้ด async I/O สำหรับ ARM M0 microcontroller ตัวเล็ก ๆ ก็ยังใช้ runtime ที่เน้น embedded อย่าง embassy และทำให้โค้ดดูแทบไม่ต่างจากที่ใช้ในสภาพแวดล้อมอื่น
      เพราะใช้ trait และ interface ชุดเดียวกัน จึงไม่ต้องสนใจรายละเอียด runtime มากนัก เมื่อเทียบกับการใช้ RTOS เล็ก ๆ หรือสร้างสภาพแวดล้อม async เองแล้วถือว่าดีมาก
      สิ่งที่เรียนรู้จากการเขียนโค้ด async ด้วย embassy ก็เอาไปใช้กับด้านอื่นได้ด้วย
    • สงสัยว่าทางเลือกคืออะไร ผมก็พอใจกับการใช้ tokio แต่ก็ดีเหมือนกันที่คนอื่นใช้ executor อื่นอย่าง smol, async-std, glommio
      ถึง tokio จะไม่ใช่ส่วนหนึ่งของ standard library แต่ก็ได้รับการดูแลอย่างดี ดังนั้นสถานการณ์ตอนนี้ก็ดูโอเค กลับกันถ้าเข้าไปอยู่ใน standard library อาจทำให้ใช้ executor ตัวอื่นยากขึ้น และอาจทำให้การพอร์ต standard library ไปแพลตฟอร์มอื่นยากขึ้นด้วย
      แน่นอนว่าความกังวลนี้อาจไม่มีมูลก็ได้
    • น่าสนใจที่พูดถึง Java เพราะ Java เองก็เคยเจอปัญหาคล้ายกันในเชิงประวัติศาสตร์
      เรื่อง logging ตอนนี้ค่อนข้างลงตัวที่ slf4j แต่ก็ยังมีไลบรารีที่ใช้อย่างอื่น ส่วน utility ทั่วไปช่วงแรกเป็น Apache Commons แล้วตอนนี้หลายแห่งใช้ Guava
      JSON ก็พอจะลงตัวที่ Jackson แต่ Gson หรือ Simple-json ก็ยังพบบ่อย และเรื่อง annotation สำหรับ nullability ก็ยังไม่เคยถูกทำให้เป็นทางการ จากเดิมเป็น distribution ไม่เป็นทางการของ JSR-305 ผ่าน checker framework แล้วช่วงหลังขยับไปทาง JSpecify
      องค์ประกอบพื้นฐานแบบนี้ภาษาควรเป็นผู้จัดให้ เพื่อเลี่ยงความแตกกระจายและการมี standard library โดยพฤตินัยหลายชุด
    • ยังมีอีกหลายด้านที่ใช้ Rust ร่วมกับ async ได้โดยไม่ต้องพึ่ง tokio จริง ๆ ดูเหมือนสิ่งที่ถูกผูกกับ tokio แบบเต็มตัวจะออกไปทาง เว็บ/เซิร์ฟเวอร์ มากกว่า
      การเขียนไลบรารีให้ไม่ผูกกับ executor ไม่ได้ยากมาก แต่ต้องระวังอย่างสม่ำเสมอ และไม่ใช่สิ่งที่คนส่วนใหญ่ในชุมชนทำกันตลอด
  • บทความยอดเยี่ยมมาก ผมชอบ การวิเคราะห์เชิงลึกด้าน optimization แบบนี้ และหวังว่าเป้าหมายของโปรเจ็กต์จะไปได้สวย
    บางครั้งรู้สึกว่าคอมไพเลอร์ไม่ได้ทุ่มแรงมากนักกับ optimization ของกรณีที่ “เล็กน้อย”
    แต่พาดหัวดราม่าเกินเนื้อหาไปหน่อย ถ้าชื่อเป็น “Async Rust Optimizations the Compiler Still Misses” ผมก็คงกดอ่านอยู่ดี

    • เลือกพาดหัวแบบนั้นเพราะมันเป็นความจริงเฉย ๆ ตั้งแต่ async เข้ามาราวปี 2019 ก็ไม่ได้มีอะไรเปลี่ยนไปมาก
      ตอนนี้ใช้ async กับ trait และ closure ได้แล้วก็จริง แต่สิ่งนั้นเป็นการอัปเดต type system ไม่ใช่การเปลี่ยนตัวกลไก async เอง ส่วน Waker ก็ใช้ง่ายขึ้นนิดหน่อย แต่ก็ใกล้เคียงกับการปรับปรุงฝั่ง std/core มากกว่า
      เท่าที่เข้าใจ คนที่ทำให้ async Rust ลงจอดได้เคยหมดไฟกันไม่น้อยและค่อย ๆ หายไปจากงานนี้ และแทบไม่มีใครมารับช่วงต่อ แต่ก็ดีใจที่อย่างน้อยฝั่ง Google มี PR หนึ่งที่พยายาม optimize memory layout ของตัวแปรที่ถูก capture ไว้
      ผมกับเพื่อนร่วมงานใช้ async เยอะมาก ดังนั้นอาจถึงเวลาที่เราต้องลงมือเอง หรืออย่างน้อยก็เริ่มต้นทำเสียที “ฟรี” น่าจะเป็นความหมายแบบที่ว่าฟรีเหมือนการเลี้ยงลูกหมา
      เพราะงั้นก็ยอมรับว่าพาดหัวออกแนว bait นิดหน่อย แต่ก็ยังไม่คิดจะถอนคำนี้
    • เห็นด้วยว่าพาดหัวเวอร์เกินไป
      ผู้เขียนดูเหมือนจะหมกมุ่นกับ overhead ของฟังก์ชันเล็กจิ๋วมากไปหน่อย เขาไม่พอใจกับ overhead ของสถานะ “panic” และ “returned” ซึ่งจริง ๆ ไม่ใช่ปัญหาใหญ่
      async block ที่มีประโยชน์ส่วนใหญ่มักใหญ่พอจน overhead ของกรณี error ถูกกลบไป
      เรื่องการ inline ที่ยังไม่ดีพออาจมีประเด็นอยู่บ้าง แต่สิ่งที่จำกัดจำนวนงานจำนวนมากจริง ๆ มักเป็น state space ที่แต่ละงานต้องใช้
  • โดยรวมแล้ว async ดูเหมือนเป็นไอเดียที่ยังไม่สุกพอ โค้ดทั่วไปก็เป็น async อยู่แล้วด้วยซ้ำ
    ถ้าต้องรอ task async สักตัว thread ก็แค่หลับไปจนกว่าจะพร้อม และ kernel ก็ abstract เรื่องนี้ให้อยู่แล้ว แต่เพราะเราไม่ชอบจัดโค้ดในรูป logical thread เลยเพิ่มระบบ callback สำหรับ event เข้ามา ก่อนจะมารู้ทีหลังว่า callback ทำให้ reasoning ยากและการควบคุมแบบลำดับตรงไปตรงมาดีกว่า
    เพราะแบบนั้นผมเลยมองว่า thread คือโมเดลการเขียนโปรแกรมที่ถูกต้อง
    ตอนนี้ language runtime ชอบ “green thread” มากกว่าเพราะเรื่อง portability และ performance แต่ภาษาส่วนใหญ่กลับไม่ได้ให้สิ่งนี้มาอย่างเหมาะสม กลายเป็นว่ามีปัญหา async/non-async coloring, scheduling, priority, non-preemption ตามมา เป็นโมเดล process และ scheduling ที่แย่กว่ายุค 1970 เสียอีก

    • ข้อความที่ว่า “โค้ดทั่วไปก็เป็น async อยู่แล้ว และเวลารอก็ให้ thread หลับโดย kernel จัดการให้” นั้นไม่แม่นนัก
      แม้แต่โค้ด async เองก็มักถูกเขียนในแบบที่ไม่ maximize concurrency ที่แสดงออกได้ เช่น แทนที่จะ “ทำ I/O ทั้ง N งานพร้อมกัน” ก็เขียนเป็น “await process(x) สำหรับแต่ละงาน X”
      แต่ในโลกของ thread ปัญหาเรื่อง concurrency นี้ยิ่งหนักกว่า เพราะ thread หนักเกินไปโดยธรรมชาติ ทำให้แสดง concurrency ได้ไม่คุ้มประสิทธิภาพ และก็ไม่มีวิธี optimize ไปในทิศทางนั้นได้จริง
      นี่ไม่ใช่บทเรียนใหม่ work-stealing executor เป็นที่รู้กันมานานแล้วว่ามี latency ต่ำกว่า thread แบบดั้งเดิมมาก และ P99 ก็สม่ำเสมอกว่า เหตุผลนี้เองที่ Apple สร้าง GCD ขึ้นมาตั้งแต่ต้นยุค 2000
      thread ไม่ได้ให้ข้อมูลที่ละเอียดพอแก่ kernel scheduler เพื่อเข้าใจลักษณะงาน และ kernel thread ก็เป็นกลไกที่หนักเกินไปสำหรับ concurrency ระดับละเอียด โดยเฉพาะเมื่อไม่ใช่งานคำนวณล้วน ๆ แต่เป็น I/O หรือภาระงานผสม
      ไม่ใช่ว่าทุกโปรแกรมต้องการ performance ระดับนี้ แต่ด้วยความพยายามเท่าเดิม การไปถึงมาตรฐานประสิทธิภาพที่สูงกว่านั้นง่ายกว่ามาก และในทางปฏิบัติก็ทำ latency กับ throughput ที่แนวทางดั้งเดิมตามได้ยากให้เกิดขึ้นได้จริง
      สัญญาณว่า async มาถูกทางยังเห็นได้จาก io_uring ด้วย io_uring ซึ่งเป็นแนวทาง I/O ประสิทธิภาพสูงของ kernel นั้นแตกต่างจาก threading และ system call แบบดั้งเดิมโดยสิ้นเชิง และการจัดการ completion ก็ใกล้กับ async concurrency มากกว่า เพียงแต่ async/await อย่างเดียวมี “สี” ไม่พอที่จะอธิบายความสัมพันธ์ระหว่าง task async ได้ทั้งหมด จึงใช้งานศักยภาพนี้ได้ยากกว่าเดิม
    • ทันทีที่ kernel กับ OS scheduler เข้ามาเกี่ยวข้อง ความเร็วอาจช้ากว่าที่ควรเป็นได้ถึง 3–4 หลัก
      ครั้งล่าสุดที่ผมเล่นกับโค้ด coroutine/scheduling การสร้าง thread ที่จบงานทันทีแล้ว join ใช้เวลาราว 200µs แต่การสร้าง green thread ของตัวเอง จัดตารางมัน แล้วรอผล ใช้แค่ประมาณ 400ns
      ไม่ต้องรออีก 10 ปีให้ใครสักคนออกแบบ async framework ที่ซับซ้อนเกินเหตุขึ้นมาใหม่ แค่ assembly 20 บรรทัดในภาษา systems ใด ๆ ก็สร้าง green thread/coroutine แบบมี stack เองได้แล้ว
    • จะบอกว่า “thread คือโมเดลการเขียนโปรแกรมที่ถูกต้อง” หรือไม่ มันขึ้นกับว่าคุณทำอะไรอยู่ สำหรับ งานที่เน้นการคำนวณ thread เหมาะกว่า แต่สำหรับ งานที่เน้น bandwidth async เหมาะกว่า
      การ optimize โค้ดที่เน้น bandwidth เป็นปัญหาเรื่องการออกแบบ schedule ในโมเดล multithreading แบบดั้งเดิมเราควบคุม scheduling ได้จำกัด แต่ในโมเดล async เราควบคุมได้แทบสมบูรณ์
      schedule แบบ async ที่ optimize มาดีจะเร็วกว่า architecture แบบ multithread ที่เทียบเท่ากันมากในงานที่เน้น bandwidth แบบเดียวกัน เรียกว่าเทียบกันแทบไม่ได้
      ทุกวันนี้โค้ดประสิทธิภาพสูงส่วนใหญ่เป็นงานที่เน้น bandwidth และ async ก็มีอยู่เพื่อทำให้ optimize workload แบบนี้ได้ง่ายขึ้น
    • ผมกลับคิดว่า callback ทำให้ reasoning ง่ายกว่า
      เวลาเทสต์การประมวลผลพร้อมกันและตรวจว่า handle race condition ได้ถูกหรือไม่ callback ง่ายกว่ามากเพราะเราควบคุม scheduling ได้ แต่ละ callback แทนหน่วยงานที่แยกจากกัน จึงมองเห็นได้ว่า event ไหน reorder ได้ และลองพิจารณาลำดับต่าง ๆ ได้ง่ายกว่า
      ในทางกลับกัน thread ทำให้ง่ายที่จะมองข้ามลำดับ และไม่ค่อยคิดว่าความซับซ้อนจาก thread อื่นจะมากระทบ thread ปัจจุบันได้เมื่อไร มันไม่ใช่ว่าง่ายกว่า แต่เป็นการทำให้ดูง่ายเกินจริง
      อีกอย่าง ถ้าไม่ใส่ barrier เทียมเพื่อหยุด thread หรือส่ง mock ที่มี callback สำหรับเปลี่ยน I/O ให้เป็น stub เพื่อควบคุมลำดับ ก็แทบเทสต์สถานการณ์ concurrent แบบเปลี่ยนเงื่อนไขจริง ๆ ไม่ได้เลย
      ปัญหาของ callback คือ captured call stack ไม่ใช่ logical call stack ดังนั้นถ้าไม่มีไลบรารี/รันไทม์ที่พยายามทำให้ call stack มีความหมาย ก็ต้องมีการนิยาม error ที่ดีมาก
      แน่นอนว่าเราก็อาจผสมสองกระบวนทัศน์นี้แล้วได้แต่ข้อเสียของทั้งคู่ก็ได้
    • thread ไม่ได้ดีกว่าหรือแย่กว่า async+callback แต่เป็น คนละโมเดล กัน มีปัญหาบางแบบที่เหมาะกับ thread และบางแบบที่การอธิบายด้วย async ดีกว่ามาก
  • ถ้าเป้าหมายหลักของ Rust คือความปลอดภัย ผมไม่เข้าใจว่าทำไมถึงมี panic อยู่ ควรจะพิสูจน์ได้ว่าในโค้ดไม่มีเส้นทางที่ panic ได้เลย
    ผมดูเรื่องนี้มาทั้งสัปดาห์แล้ว และการสร้างโปรแกรมที่รับประกันว่าไม่ panic เลยนั้นยากมาก เท่าที่เข้าใจ panic handler มีขนาดราว 300KB และวิธีเดียวที่จะตัดมันออกได้คือตอนคอมไพล์ต้องไม่มีเส้นทางใดในโค้ดที่ panic ได้เลย การไปเช็กหลังคอมไพล์ว่า panic handler ถูกใส่มาในไบนารีหรือไม่มันให้ความรู้สึกเหมือนแฮ็ก
    คุณอาจใช้ lint เพื่อห้าม unwrap และการกระทำที่ panic อื่น ๆ ได้ แต่ถ้ามี subset ของ Rust แบบ no-panic ปัญหาส่วนใหญ่ที่บทความนี้พูดถึงก็คงหายไป
    ในความเป็นจริง หลายกรณีจะไม่เกิดขึ้นเลยเว้นแต่ระดับบิตพลิก แต่การต้องรับมือกับภาษาที่มี operation จำนวนมากซึ่งในทางทฤษฎี panic ได้ก็ชวนหงุดหงิด ไม่ว่าจะเป็นการพิสูจน์ว่า array ไม่ว่างหรือการรับมือกับ async
    สุดท้ายจึงต้องใส่การจัดการ error จำนวนมากสำหรับกรณีที่ไม่มีทางเกิด หรือไม่ก็ใช้โครงสร้างประหลาดอย่างแพตเทิร์นรายการที่ไม่ว่างซึ่งแยก field แรกออกจากรายการที่เหลือ แล้วโครงสร้างนั้นก็เพิ่มภาระบวมของมันเองอีก

    • ฝั่ง Rust-in-Linux กำลังจัดการปัญหานี้ด้วยเรื่องอย่าง operation หน่วยความจำที่อาจล้มเหลว ซึ่งเป็นฟีเจอร์ที่พวกเขาจำเป็นต้องมี
      งานด้านการเพิ่ม การใช้งานแบบอิงการพิสูจน์ รวมถึงการพิสูจน์ว่า array ไม่ว่าง ก็กำลังค่อย ๆ เดินหน้าอยู่เช่นกัน
    • panic สำคัญมากต่อ usability และ safety
      ถ้าไม่มี panic และต้องบังคับให้โปรแกรมทำงานต่อในทุกสถานการณ์ คุณก็ต้องใส่การจัดการ error จำนวนมหาศาลในทุกจุดที่ตรวจ invariant เพื่อพยายามกู้คืนจากสถานการณ์อย่าง memory corruption ที่ invariant พังไปแล้ว
      ซึ่งมันก็คือปัญหาแบบเดียวกันกับที่คุณกังวลอยู่พอดี คือการต้องแบก error handling มหาศาลสำหรับเหตุการณ์ที่แทบไม่มีวันเกิด
    • เป้าหมายของ Rust คือ memory safety และในมุมของ memory safety นั้น panic ปลอดภัยอย่างสมบูรณ์
    • แม้แต่ OS ที่รันโปรแกรมของคุณก็ยังไม่สมบูรณ์แบบ
      มันน่าเหนื่อยกับท่าทีที่อยากให้เครื่องมือทำให้ทุกอย่าง fail-proof โดยที่ตัวเองไม่อยากลงมือทำอะไรเลย อยากได้ API ที่ง่าย ถ้ายังไม่ง่ายพอก็อยากได้ Kubernetes container ที่ “เขียนโปรแกรม” ด้วย YAML และถ้ายังไม่ง่ายพอก็อยากได้บริการโฮสต์แบบคลิก ๆ ของ GCP หรือ Amazon
      สุดท้ายมันไม่ค่อยเหมือนอยากเขียนโปรแกรม แต่เหมือนอยากบริโภคแอปที่ไม่มีวันพังมากกว่า และวิถีแบบนั้นก็อาศัยความสัมพันธ์เชิงพึ่งพากับคนที่สร้างของเหล่านี้ให้อยู่ดี
  • การถกเถียงแบบ น่าเกลียดแต่จำเป็น เช่นนี้ก็มีอยู่ใน C++ มาพักใหญ่แล้ว
    ตั้งแต่ Rust เอา async เข้ามา ผมก็ไม่ค่อยชอบความเป็นสิ่งที่แพร่ติดเหมือนโรคของมันนัก
    ผมหวังให้ Rust ไปได้ดี และถ้ามีคนแบบนี้มากขึ้น อนาคตของ Rust ก็น่าจะสดใสขึ้น

  • ผมเพิ่งเริ่มทำงาน async ใน Rust ไม่นาน และปัญหาหลักที่เจอตอนนี้คือ การทำโค้ดซ้ำ
    ทุกฟังก์ชันที่อยากรองรับทั้ง async API และ blocking API ต้องเขียนซ้ำทั้งคู่ maybe-async ฟังดูน่าจะดีมาก
    ผมลองดู crate อย่าง maybe-async, bisync เพื่อหาทางเลี่ยง แต่ทั้งหมดมีปัญหาหรือข้อจำกัดค่อนข้างแรง

    • กำลังมีงานเรื่อง keyword generics ที่จะทำให้สร้างฟังก์ชันแบบ generic กับคีย์เวิร์ดอย่าง async หรือ const ได้
      ตอนนี้ตัวเลือกที่ดีที่สุดสำหรับการเขียนโค้ดที่อยากอยู่ได้ทั้งโลก synchronous และ asynchronous คือ sans-io Thomas Eizinger จาก Fireguard เขียนบทความดี ๆ เกี่ยวกับแพตเทิร์นนี้ไว้[1]
      แพตเทิร์นนี้ไม่เพียงแก้ปัญหา sync/async ได้อย่างสะอาด แต่ยังทำให้เทสต์ง่ายขึ้น และเปิดทางไปสู่เทคนิคอย่าง DST ได้ด้วย[2]
      ผมเองก็เขียนบทความเกี่ยวกับหัวข้อนี้ไว้[3] โดยเน้นว่าปัญหานี้กว้างกว่าแค่ async vs sync เพราะยังรวมถึง executor คนละตัวด้วย
      0: https://github.com/rust-lang/effects-initiative
      1: https://www.firezone.dev/blog/sans-io
      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
      3: https://hugotunius.se/2024/03/08/on-async-rust.html
    • มันขึ้นกับว่าจริง ๆ แล้วคุณทำอะไร แต่ถ้าง่ายพอ อาจทำ macro ที่คอยสลับ type กับ await ให้แทนได้
    • นี่คือ ปัญหา function coloring แบบคลาสสิก https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • จากมุมผม async function นั่นแหละคือ maybe-async อยู่แล้ว
      ความต่างระหว่าง fn -> void กับ fn -> Future คืออันแรกทำงานจนจบทันที ส่วนอันหลังอาจไปจบทีหลัง
      ถ้าอยากรัน async function แบบ blocking ก็ใช้ blocking executor ได้เลย
  • สิ่งที่ทำให้ผมชอบบทความนี้คือมันทำให้ได้เห็นแม้กระทั่ง เป้าหมาย Rust ปี 2026
    ทีมของผมใช้ Rust แต่ไม่จำเป็นต้องลงลึกมากเพื่อทำงานที่ต้องการ ถึงอย่างนั้นการได้เห็นภาษาที่มี feedback จากชุมชนมากพัฒนาจากฐานรากก็ยังสนุกดี
    กับ C++ ผมไม่ค่อยรู้สึกถึงกระแสแบบนี้ และในพื้นที่อื่นเขาทำกันอย่างไรก็ไม่ค่อยทราบ
    เพียงแต่สิ่งที่น่าเสียดายคือดูเหมือนแต่ละเป้าหมายต้องมีการระดมทุนเฉพาะทาง ทำให้อารมณ์คล้าย Kickstarter นิด ๆ เลยสงสัยว่านี่คือโมเดลที่ดีที่สุดที่หาได้แล้วหรือยัง

    • คำว่า “เป้าหมายของโปรเจ็กต์” ทำให้เข้าใจผิดได้มากกว่าความหมายจริงพอสมควร
      project goal คือระบบที่ให้บุคคลหนึ่งคนหรือกลุ่มเล็ก ๆ แสดงว่าพวกเขาอยากทำงานบางอย่าง และขอเวลาสนับสนุนอย่างต่อเนื่องจากอาสาสมัครของโปรเจ็กต์ Rust เช่น การรีวิวโค้ดหรือช่วยตอบคำถาม
      มันไม่ได้แปลว่าตัวโปรเจ็กต์ Rust เองเป็นผู้ตั้งเป้าหมายนั้น หรือสนับสนุนมันอย่างเป็นทางการเสมอไป
      ดังนั้นจึงไม่ค่อยถูกนักถ้าจะมองสิ่งนี้เป็น roadmap ทางการของ Rust และจะถูกกว่าถ้ามองว่าเป็น “มีผู้มีส่วนร่วมที่อยากทำงานในพื้นที่นี้”
    • ดูเหมือนแม้แต่ในคณะกรรมการ ISO ของ C++ เองก็มีฉันทามติระดับหนึ่งว่ากระบวนการวิวัฒนาการของภาษานี้ค่อนข้างพังแล้ว สาเหตุหลักคือขนาดและวิธีจัดองค์กร
      พอเทคโนโลยีกลายเป็นสิ่งที่มีผลประโยชน์ทางการค้า ก็น่าเสียดายที่มันมักไหลไปในทิศทางนี้ จะไปโทษสปอนเซอร์รายใหญ่ที่สนับสนุนเฉพาะส่วนที่ตัวเองสนใจก็ยาก
      โชคดีที่เงินทุนจำนวนหนึ่งของ TweedeGolf มาจากรัฐบาลเนเธอร์แลนด์ตามที่ผมเข้าใจ
    • งานโอเพนซอร์สน่าจะมีอยู่คร่าว ๆ สองประเภท: การพัฒนาฟีเจอร์ และการบำรุงรักษา
      ฟีเจอร์ใหม่ “ขาย” ได้ การสร้างมันมีต้นทุน แต่ถ้ามันแก้ปัญหาจริง และต้นทุนของปัญหานั้นสูงกว่าค่าใช้จ่ายในการพัฒนาฟีเจอร์ บริษัทก็มักยอมจ่าย
      งานบำรุงรักษายากกว่า แต่ตอนนี้ก็มีกองทุนสำหรับ maintainers แล้ว ตัวอย่างเช่นกองทุนของ RustNL: https://rustnl.org/maintainers/
      กองทุนลักษณะนี้รองรับงานที่กว้างและต่อเนื่องกว่า โดยให้องค์กรหลายแห่งช่วยกันลงเงินทีละน้อย
      ไม่แน่ใจว่านี่คือโมเดลที่ดีที่สุดหรือไม่ แต่อย่างน้อยก็ดูเหมือนพอใช้การได้ในระดับหนึ่ง
  • ถ้าอ่านเอกสารของ Rust Async และ Tokio จะเห็นว่าอธิบายไว้ค่อนข้างดีว่าทำไมไม่ควรเอาส่วนที่ใช้ CPU หนักไปวางไว้ใน async stack, วิธีใช้เครื่องมือพื้นฐานอย่าง std::sync::Mutex ให้มีประสิทธิภาพภายใน async block, และวิธีเชื่อมโค้ด synchronous กับโค้ด async เข้าด้วยกัน
    โค้ดจำนวนมากไม่ได้สนใจหรือไม่จำเป็นต้องสนใจเรื่องประสิทธิภาพ จึงไม่เดินตามแนวทางเหล่านี้ แต่ก็มีหลายโปรเจ็กต์ที่ให้ความสำคัญกับ performance และ efficiency และพอเอาโค้ดไปรันจริงใน production ก็จะเจอกับดักเหล่านี้ ScyllaDB เป็นตัวอย่างหนึ่ง
    LLM ก็ไม่ช่วย มันสร้างทุกอย่างให้เป็น async ไปจนถึง main, ใช้เครื่องมือพื้นฐานผิดตัว และไม่ออกแบบระบบให้ถูกต้อง

  • เรื่องการพับสถานะซ้ำ ๆ หรือก็คือ แพตเทิร์นดึง match ออกมาไว้นอกแขนงที่มี await แบบในตัวอย่าง process_command นั้น น่าจะเป็นวิธีที่ง่ายที่สุดที่ใครก็เอาไปใช้กับโค้ด async เดิมได้ตั้งแต่วันนี้
    ไม่ต้องรอให้คอมไพเลอร์ทำอะไร แค่รีแฟกเตอร์โค้ดเท่านั้น

    • อย่างน้อยก็ควรมี lint แบบกำหนดเอง ที่ช่วยหาว่าจุดไหนใช้ได้บ้าง ซึ่งระดับนั้นก็ใกล้เคียงกับเป็นงานฝั่งคอมไพเลอร์แล้ว
  • เกี่ยวกับประเด็น “Future ไม่ถูก inline ได้ง่าย” ในภาษาโปรแกรมที่ผมสร้างเอง ผมเคยเขียน custom pass เพื่อ inline การเรียก async function ภายใน async function
    โดยรวมแล้วมันทำงานได้ดีและตัด boilerplate ออกได้บางส่วน แต่ขนาดไบนารีที่ได้ใหญ่ขึ้นมาก
    ในเชิงเทคนิค Rust ก็น่าจะทำแบบเดียวกันได้

 
GN⁺ 2026-05-06
ความเห็นจาก Lobste.rs
  • พอเห็นแค่ชื่อเรื่องก็คิดไว้แบบหนึ่ง แต่บทความนี้ สร้างสรรค์กว่าที่คาดไว้มาก

    • มองว่าใกล้เคียงข้อเท็จจริงเลย หลังจาก เปิดตัว MVP มา 7 ปี ก็แทบไม่มีความคืบหน้าเลยทั้งด้านการออกแบบภาษาและการพัฒนาคอมไพเลอร์ และเพราะคนที่เป็นกำลังหลักในการทำ MVP ต่างก็ลดบทบาทในโปรเจ็กต์ลงในช่วงเวลาใกล้กัน ทำให้การส่งต่องานหลังจากนั้นแทบหยุดชะงัก
      หวังว่าคนที่อยากทำงานนี้จะได้รับการสนับสนุนที่จำเป็น
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    ดีใจที่เห็นว่าปัญหานี้กำลังถูกหยิบมาจัดการ ก่อนหน้านี้เคยเห็นหลายบทความบอกว่า rustc โยนโค้ดให้ LLVM มากเกินไป แล้วคาดหวังให้ตัว optimize จัดการทุกอย่างให้ โดยเฉพาะบทความนี้ยังขอ เงินสนับสนุน สำหรับงานดังกล่าวด้วย

  • โอ้โห ฉันนี่โง่เอง
    ฉันคิดมาตลอดว่า async มัน “อ้วน” โดยเนื้อแท้ เพราะไม่ว่ารูปแบบไหนก็ต้องมี runtime, การติดตามงาน, และการ polling เพื่อตรวจว่าทำเสร็จหรือยัง
    เพราะ overhead นั้นมันไม่เป็นศูนย์อยู่แล้ว ฉันเลยมองว่า “zero-cost abstraction” ที่พูดกันเป็นเรื่องของฟีเจอร์ภาษา และแยกจาก runtime ที่พ่วงเข้ามา
    ไม่เคยนึกเลยว่าจะต้องไปดูว่า rustc ปล่อยอะไรออกมาก่อนส่งให้ LLVM

  • สำหรับคนที่ไม่คุ้นกับ async Rust:

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    อันนี้จริงมาก ต้นไม้ของการเรียก async ที่ซ้อนกัน แม้จะลึกแค่ไหน ถ้าผ่านการ optimize เต็มที่แล้วก็จะยุบกลายเป็น struct เดียว ที่มี state machine อยู่ข้างใน เป็นวิธีที่ฉลาดมากจริงๆ

  • ถ้าไปถึงกรณีนี้ใน release build มันจะกลายเป็น deadlock แบบหนึ่งหรือเปล่า? หรืออาจเกิดการรั่วเพราะมี task ที่รอ future ที่เป็น Pending ตลอดเวลา?

    • ใช่ future แบบนั้นจะ ค้างอยู่ และไม่มีวัน complete แต่สถานะแบบนี้จะไปถึงได้เฉพาะในโค้ด async ระดับล่างที่มีบั๊กอยู่แล้วเท่านั้น และโค้ดที่ติดตาม future ที่ complete แล้วได้ไม่ถูกต้อง ก็น่าจะมีแนวโน้มก่อทั้งการรั่วและ deadlock อยู่แล้ว
      การ poll ผิดผ่าน .await ทำไม่ได้
  • มีความคิดอยู่สองสามข้อ:

    1. บทความนี้ดูเหมือนกำลังบอกว่าควรย้าย logic การ optimize ออกมาจาก LLVM ให้มากขึ้น แล้วเอามาไว้ที่ระดับ MIR แทน เช่น พอจะเข้าใจได้ว่าทำไมการ inline ฟังก์ชัน async ถึงทำใน MIR ได้ง่ายกว่าใน LLVM ถ้าทำกับ async ได้แล้ว ก็สงสัยว่าควร generalize logic นั้นไปยังฟังก์ชัน synchronous ด้วยไหม แล้วค่อยตัด optimization pass บางส่วนออกจาก LLVM แม้จะรู้ว่าเป็นงานใหญ่มาก และคำถามนี้ก็เป็นเรื่องทิศทางมากกว่าความเป็นไปได้เชิงปฏิบัติ พอคอมไพเลอร์ส่วน frontend/middle-end ซับซ้อนถึงระดับหนึ่ง ก็ดูมีเหตุผลที่ optimization แบบทั่วไปของ LLVM จำนวนมากอาจควรถูกย้ายไปทำที่อื่นแทน
    2. ฉันก็ยังไม่ชอบ panic=unwind อยู่ดี นอกจาก test harness บางแบบแล้ว แทบไม่เคยเห็นข้อดีที่มากพอจะชดเชยต้นทุนเมื่อเทียบกับ panic=abort เลย แม้แต่ test harness เอง บน Linux ก็ดูเหมือนจะใช้ทางเลือกคล้ายกันได้ เช่น ใช้ clone แบบแปลกๆ แล้ว wait เฉพาะ thread ที่รัน แทน pthread_join ตรงนี้ฉันอาจเข้าใจผิดก็ได้
  • ลิงก์นี้เพิ่งใช้ไม่ได้สำหรับคนอื่นด้วยไหม?
    แก้ไข: หน้า blog post โผล่มาได้ประมาณครึ่งวินาทีแล้วก็เด้งไปหน้า 404
    แก้ไข 2: เข้าไปที่รายการบทความในบล็อกแล้วลองกดไปหลายอัน พอเปิดบทความนั้นจากในรายการก็ยังไปหน้า 404 เหมือนเดิม จะทำบล็อกที่เป็น static page หรืออย่างน้อยควรจะเป็นแบบนั้น ให้พังได้ขนาดนี้ยังไงกัน?

    • น้ำเสียงดูหยาบและก้าวร้าวเกินจำเป็นนิดหน่อย เว็บไซต์ก็มีบั๊กกันได้ การรายงานปัญหานั้นมีประโยชน์ แต่คอมเมนต์นี้ฟังดูประชดประชันไปหน่อย
      สำหรับข้อมูลเพิ่มเติม ฉันลองทำตามขั้นตอนเดิมเหมือนกันแล้ว แต่ไม่เจอ 404 เลย ทั้งบนมือถือและเดสก์ท็อป และลองทั้งเปิดกับปิด JavaScript แล้วด้วย เพราะงั้นอาการที่เจออาจซับซ้อนกว่าที่เห็นก็ได้