4 คะแนน โดย GN⁺ 2024-03-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • async/await ของ Rust ไม่ใช่สิ่งทดแทนเธรดแบบง่าย ๆ แต่เป็นโมเดลการเขียนโปรแกรมที่แสดงโค้ด concurrency ที่เน้น I/O ให้เป็น state machine ที่ประกอบรวมกันได้
  • โค้ดอย่างเว็บเซิร์ฟเวอร์ที่ต้องจัดการหลาย connection พร้อมกันจะมีข้อจำกัดหากอาศัยการรันแบบเชิงเส้นเพียงอย่างเดียว ส่วนเธรดใช้ thread::spawn เพื่อแยกการประมวลผลไคลเอนต์ออกไป ทำให้ ประมวลผลพร้อมกัน ได้
  • async/await จะยอมสละการรัน ณ จุด await แล้วให้ executor รันงานอื่นต่อ ทำให้งานจำนวนมากถูกรันสลับกันภายใน runtime เดียว
  • ข้อกำหนดอย่าง timeout 3 วินาทีสามารถเพิ่มใน async ได้ด้วยการประกอบ race กับ Timer แต่ในโค้ดเธรดแบบ synchronous ต้องใช้ wrapper เฉพาะสำหรับ TcpStream และตั้งค่า timeout สำหรับการอ่าน/เขียน ทำให้ ความเป็นทั่วไป ลดลง
  • หากอธิบาย async ด้วย overhead ด้านประสิทธิภาพเพียงอย่างเดียว จะเกิดตัวอย่างโต้แย้งในงานแบบ CPU bound จุดแข็งของ Rust async จึงอยู่ที่ พลังในการสื่อความหมายเชิง semantics และความสามารถในการประกอบกับ ecosystem

จุดเริ่มต้นของปัญหา concurrency ใน Rust

  • โค้ด Rust ทั่วไปโดยพื้นฐานเป็นโครงสร้างการรันแบบเชิงเส้น
    • งานหนึ่งเสร็จแล้วจึงรันงานถัดไป เช่น foo(), bar(), baz()
  • ในกรณีที่ต้องจัดการหลายงานพร้อมกันอย่างเว็บเซิร์ฟเวอร์ โครงสร้างเชิงเส้นจะชนข้อจำกัดอย่างรวดเร็ว
    • ในโครงสร้างที่รับไคลเอนต์ด้วย TcpListener::accept() แล้วรัน handle_client() ไคลเอนต์รายที่สองต้องรอระหว่างที่กำลังจัดการไคลเอนต์รายแรก
    • หาก handle_client() ใช้เวลาไม่กี่มิลลิวินาทีและมีไคลเอนต์พร้อมกัน 2 ราย ก็จะเกิดการรอช่วงสั้น ๆ
    • หากมีไคลเอนต์พร้อมกัน 2 ล้านราย ผู้ใช้ที่อยู่ท้ายคิวอาจต้องรอหลายนาที

วิธีที่เธรดแก้ปัญหา

  • เธรดของระบบปฏิบัติการสามารถบันทึกค่า register และ program stack ไว้ในหน่วยความจำ รัน routine อื่น แล้วกลับมารัน routine เดิมต่อภายหลังได้
  • โค้ดเว็บเซิร์ฟเวอร์มอบหมายการจัดการไคลเอนต์ให้เธรดแยกต่างหากในรูปแบบ thread::spawn(move || handle_client(client))
    • เธรดหลักยังคง accept() connection ใหม่ต่อไป
    • หากเธรดที่จัดการไคลเอนต์ถูกบล็อก OS จะกลับมาที่เธรดหลักเพื่อรับ connection ถัดไปได้
    • ไคลเอนต์สองรายสามารถรันแบบขนานกันได้หลังจากมี latency ระดับไม่กี่ไมโครวินาที
  • หากเว็บเซิร์ฟเวอร์ระดับ production มี CPU core หลายสิบ core OS ไม่เพียงทำให้ดูเหมือนว่าเธรดต่าง ๆ รันพร้อมกัน แต่ยังสามารถรันหลายเธรดพร้อมกันจริง ๆ ได้ด้วย

async/await ทำงานอย่างไร

  • concurrency ใน user space มีหลายโมเดล เช่น event-driven programming, actor, coroutine และแนวทางที่ Rust เลือกคือ async/await
  • หากอธิบายแบบง่าย โปรแกรมจะถูกคอมไพล์เป็นชุดของ state machine ที่สามารถรันแยกจากกันได้
    • async fn ไม่ใช่ฟังก์ชันแบบดั้งเดิม แต่เป็นฟังก์ชันที่คืนค่า state machine
    • await รวม state machine อื่นเข้ามาเป็นขั้นตอนหนึ่งของ state machine ปัจจุบัน
    • เมื่อฟังก์ชันภายในยอมสละการรัน เช่น ระหว่างรอ connection ใหม่ state machine ทั้งหมดจะส่งสิทธิ์ควบคุมกลับไปยัง executor ชั้นบน
  • executor อย่าง smol::Executor จะรัน state machine อื่นที่สร้างด้วย spawn แทน state machine ปัจจุบัน
    • บล็อก async move { handle_client(client).await } คือ state machine ใหม่ที่เป็นอิสระจาก main
    • เมื่อ main ยอมสละการรัน งานของไคลเอนต์งานหนึ่งจะถูกรัน และเมื่องานนั้นยอมสละการรันอีก ก็จะวนไปยังงานถัดไป
  • โครงสร้างนี้ทำให้จัดการไคลเอนต์พร้อมกันได้หลายล้านราย แต่ก็เพิ่มความซับซ้อนจากแนวคิดอย่าง executor, task และ state machine

ความสามารถในการประกอบที่เห็นได้จากตัวอย่าง timeout

  • จุดแข็งอย่างหนึ่งของ Rust คือ ความสามารถในการประกอบ
    • Iterator สามารถต่อ combinator หลายตัว แล้วส่งผลลัพธ์ต่อไปยังฟังก์ชันที่รับ Iterator ได้
    • สามารถกรองและแปลงค่าแล้วเพิ่มลงใน list ได้ เช่น recv.try_iter().filter(...).map(...) ของ mpsc::channel()
  • async/await ทำให้สามารถนำความสามารถในการประกอบแบบนี้ไปใช้กับฟังก์ชันแบบ I/O bound ได้ด้วย
  • หาก handle_client() เป็นฟังก์ชัน asynchronous ที่ await read_to_end, do_something_with_data, write_all timeout 3 วินาทีสามารถทำได้โดยประกอบ Future สองตัวเข้าด้วยกัน
    • race รัน Future สองตัวพร้อมกัน
    • Timer คืนค่าหลังเวลาที่กำหนดผ่านไป
    • เพียงห่อโค้ดจัดการ connection เดิมด้วยบล็อก async แล้วให้แข่งกับ Future ที่คืน error TimedOut หลัง 3 วินาที
  • วิธีนี้ไม่ถูกผูกไว้กับ TcpStream เท่านั้น
    • หากเป็นสิ่งที่ implement impl AsyncRead + AsyncWrite ก็สามารถใช้ pattern เดียวกันได้
    • สามารถแทนที่ด้วยเป้าหมายอย่าง GZIP stream บน stream ทั่วไป, Unix socket หรือไฟล์ได้ด้วย

ข้อจำกัดเมื่อ implement timeout แบบเดียวกันในโค้ดเธรด synchronous

  • ในโค้ดแบบ blocking โดยทั่วไปยากที่จะหยุด system call อย่าง read หรือ write และวิธีอย่างการปิด file descriptor ก็ใช้ไม่ได้ใน Rust
  • TcpStream มี set_read_timeout และ set_write_timeout
    • สามารถตั้งค่า timeout แยกกันสำหรับการอ่านและการเขียนได้
    • แต่หากไคลเอนต์ส่งข้อมูลทีละ 1 ไบต์ทุก 2.9 วินาที timeout แบบง่ายอาจถูกรีเซ็ตต่อไปเรื่อย ๆ
  • เพื่อป้องกันกรณีนี้ ต้องสร้างชนิดอย่าง DeadlineStream ที่ห่อ TcpStream แล้วคำนวณเวลาที่เหลือจนถึง deadline ทั้งหมดทุกครั้งเพื่อตั้งค่า timeout สำหรับการอ่าน/เขียน
  • แนวทางนี้ทำงานได้ แต่มีข้อจำกัดมาก
    • ผูกอยู่กับ TcpStream
    • Rust ไม่มี trait สำหรับ abstract การใช้ set_read_timeout และ set_write_timeout
    • หากจะนำไปใช้กับ writer ทั่วไปต้องทำงานเพิ่มเติมอีกมาก
    • มี system call เพิ่มเติมสำหรับการตั้งค่า timeout
    • ใน logic ของเว็บเซิร์ฟเวอร์จริงอาจใช้งานยุ่งยากกว่า

ตัวอย่างจาก ecosystem ของ Rust async

  • เหตุผลหนึ่งที่ ecosystem ของ HTTP รวมถึงฝั่ง client เลือก async/await เป็นกลไก runtime หลัก คือความสามารถในการประกอบฟังก์ชัน
    • ฟังก์ชันที่สร้าง HTTP call สามารถเสียบเข้ากับช่องว่างและกรณีใช้งานหลากหลายได้
  • tower เป็นตัวอย่างสำคัญที่แสดงความสามารถในการประกอบของ async/await
    • หาก implement service เป็นฟังก์ชัน async ก็สามารถเพิ่ม timeout, rate limiting, load balancing, hedging และการจัดการ backpressure ได้
    • ไม่ว่าจะใช้ runtime ใด หรือภายใน service ทำอะไร ก็สามารถใช้ tower เพื่อเพิ่มความทนทานได้
  • macroquad เป็น game engine ขนาดเล็กสำหรับ Rust และรัน engine โดยใช้ async/await ในฟังก์ชัน main
    • async/await เหมาะสำหรับแสดงสถานการณ์ที่ต้องหยุดฟังก์ชันเชิงเส้นเพื่อรอบางงานใน Rust
    • สามารถจัดโครงสร้างให้ polling connection เครือข่ายของ game server และ GUI framework พร้อมกันบนเธรดเดียวกันได้

ข้อจำกัดของการอธิบาย async ด้วยประสิทธิภาพเพียงอย่างเดียว

  • Rust Async Book เปรียบเทียบว่า OS thread ทำให้แสดง concurrency ได้ง่ายโดยไม่ต้องเปลี่ยนโมเดลการเขียนโปรแกรม แต่การ synchronize ระหว่างเธรดทำได้ยาก มี overhead ด้านประสิทธิภาพสูง และแม้ใช้ thread pool ก็ยังรองรับ workload แบบ I/O bound ขนาดใหญ่ได้ไม่เพียงพอ
  • ในชุมชน async มีแนวโน้มที่จะตอบคำถามว่าทำไมถึงใช้ async แทน OS thread ในทำนองว่า “overhead ต่ำกว่า และอย่างอื่นเหมือนกัน”
  • เหตุผลที่ผู้เขียนเว็บเซิร์ฟเวอร์ย้ายมาใช้ async/await คือเพื่อแก้ C10k problem แต่เหตุผลที่ผู้ใช้ทุกคนเลือก async/await ไม่จำเป็นต้องเป็นเรื่องประสิทธิภาพเสมอไป
  • ข้อได้เปรียบด้านประสิทธิภาพอาจหายไปได้ตามสถานการณ์
    • ในงานแบบ CPU bound workflow ที่อิงเธรดอาจเร็วกว่า workflow async ที่เทียบเท่ากัน
    • ข้อได้เปรียบด้านประสิทธิภาพชั่วคราวของ Rust async ถูกเน้นมากเกินไป ขณะที่ข้อได้เปรียบเชิง semantics ถูกประเมินต่ำเกินไป
  • async/await ไม่ใช่เครื่องมือสำหรับกรณีเฉพาะกลุ่มเล็ก ๆ แต่เป็นโมเดลการเขียนโปรแกรมที่ทรงพลังสำหรับจัดการ pattern ที่ยากจะแสดงอย่างกระชับใน Rust แบบ synchronous หากไม่มีเธรดและ channel จำนวนมาก

ยอมรับความแตกต่าง แทนที่จะทำให้เหมือน sync Rust

  • ใน roadmap ของโปรเจกต์ Rust มีทิศทางว่า การเขียน async Rust ควรทำได้ง่ายเท่าการเขียนโค้ด synchronous นอกเหนือจากการใช้คีย์เวิร์ด async และ await เป็นครั้งคราว
  • แต่ก็มีมุมมองว่า framing ให้ async Rust “เหมือนกับ sync Rust ทุกประการ” นั้นยากโดยพื้นฐาน
    • แม้จะทำให้คล้ายกันได้ถึง 99% ผู้ใช้ทั่วไปก็ยังต้องสังเกตเห็นความแตกต่างอยู่ดี
  • ecosystem ของ async/await ใน Rust ควรแสดงจุดแข็งอย่าง ความสามารถในการประกอบ และ พลังในการสื่อความหมาย ให้ชัดเจนขึ้น แทนที่จะพยายามทำให้เหมือน Rust แบบ synchronous
  • หากต้องการให้ async/await กลายเป็นตัวเลือกพื้นฐานเมื่อจำเป็นต้องใช้ concurrency ควรอธิบายโมเดลนี้ด้วยเหตุผลเชิง semantics มากกว่าเหตุผลเชิงประสิทธิภาพทางเทคนิค

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

 
GN⁺ 2024-03-26
ความเห็นจาก Hacker News
  • async/await แบบเธรดเดียว เป็นโมเดลที่เรียบง่ายและเป็นที่รู้จักดี และ JavaScript ก็ใช้แนวทางนี้
    เธรดช่วยให้ดึง CPU หลายตัวมาร่วมแก้ปัญหาได้ และ Rust ก็ช่วยจัดการเรื่องล็อกให้ สามารถมีเธรดที่มีลำดับความสำคัญต่างกันได้ด้วย ดังนั้นสำหรับงานที่ติดขีดจำกัดด้านการคำนวณก็อาจจำเป็น
    ในทางกลับกัน async/await แบบหลายเธรด จะเลอะเทอะขึ้น หากมีช่วงที่เป็นงานคำนวณหนักอย่างจริงจัง มันแทบจะไปบล็อกเธรดที่ใช้ร่วมกับงานอื่น ทำให้โมเดลพังได้ง่าย
    การคำนวณแบบหลายเธรดของ Rust ไม่ได้ทำงานดีอย่างที่คาดไว้เสมอไป ในตัวจัดสรรหน่วยความจำ หลายเธรดอาจไปกระแทกล็อกเดียวกันจนเกิด futex congestion collapse และโดยเฉพาะเมื่อขยายบัฟเฟอร์ หากมีการคัดลอกซ้ำขณะล็อกตัวจัดสรรทั้งหมดไว้ จะมีต้นทุนสูงมาก ตัวจัดสรรของไลบรารีภายใน .DLL ที่อีมูเลตไลบรารี Microsoft ของ Wine เปราะบางต่อปัญหานี้ ทำให้เวลา CPU ทั้งหมดถูกใช้ไปกับ spinlock และประสิทธิภาพตกลงเป็นสิบเท่า ขณะที่ implementation ของ Microsoft ไม่เป็นแบบนั้น
    อีกทั้ง Mutex มาตรฐานและ channel ของ crossbeam-channel อาจเกิด unfair mutex starvation ได้ เมื่อหลายเธรดล็อกทรัพยากร ทำงาน แล้วปลดล็อกซ้ำ ๆ เธรดหนึ่งอาจชนะอยู่ตลอด ส่วนที่เหลือถูกดันออกไป หากต้องการ mutex ที่ยุติธรรมก็มี parking-lot แต่จะไม่มีความปลอดภัยแบบ poisoning เมื่อเธรด panic ที่ mutex มาตรฐานให้มา
    ถ้าไม่ใช่งานที่ติดขีดจำกัดด้าน I/O เรื่องจะซับซ้อนขึ้นมาก
    https://users.rust-lang.org/t/mutex-starvation/89080

    • ถูกต้อง แม้ส่วนใหญ่จะพูดถึงเฉพาะการคำนวณที่ติดขีดจำกัดด้าน I/O แต่แม้ตรงนั้นก็ยังเกิด ปัญหาการแย่งชิงทรัพยากร
      ถ้า throughput ของ I/O กลับมาเป็นขีดจำกัดอีกครั้ง coroutine เป็นล้านตัวจะมีความหมายอะไร ถ้าคุณใช้ connection pool ของ DB ขนาด 10 จนหมดทันที coroutine ก็ช่วยไม่ได้ แถมทำให้การดีบักและการหาทางเลี่ยงยากขึ้น และให้เหตุผลเกี่ยวกับระบบได้ยากขึ้นด้วย
    • ผมคิดว่าปัญหานี้อาจต้องย้อนกลับไปคิดใหม่ในระดับฮาร์ดแวร์ในที่สุด
      ปัญหาที่ติดขีดจำกัดด้าน CPU ดูเหมือนจะลงเอยที่การ interrupt/resume อย่างเป็นระบบ และถ้าสามารถทำ context switch แบบอิงคิวที่ยุติธรรมและมีประสิทธิภาพสำหรับเธรดการทำงานที่กำลังดำเนินอยู่ n เธรดได้ เช่น CPU ที่มี context ที่กำลังดำเนินอยู่ n ชุด ผมก็สงสัยว่าปัญหานี้อาจกลายเป็นปัญหา การจัดสรรทรัพยากร ได้หรือไม่
    • ไม่เข้าใจว่าทำไมความยากของ cooperative multitasking ถึงถูกค้นพบซ้ำอยู่เรื่อย ๆ
      แม้แต่ Go ซึ่งถือว่าเป็นภาษาที่ออกแบบอย่างมีความรับผิดชอบ ตอนแรกก็ไปทาง cooperative แต่สุดท้ายก็จำเป็นต้องเปลี่ยนเป็น preemptive นี่ไม่ได้หมายความว่า cooperative multitasking ไม่มีประโยชน์ แต่ควรมีป้ายเตือนติดไว้ และยิ่งไปกว่านั้น บางทีควรบล็อกการรันโค้ดบางประเภทแบบ statically ไปเลย
      แนบบทความที่เกี่ยวข้อง “What color is your function” ไว้ด้วย
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • ต้องพูดซ้ำอยู่เรื่อย ๆ ว่านี่เป็น รายละเอียดของ implementation
      executor แบบหลายเธรดของ async/await สามารถจัดการ starvation ได้ดีพอ และ implementation ของ .NET ก็ทนโค้ดที่แย่มากซึ่งผสมการเรียกแบบ blocking กับ async ได้
      https://news.ycombinator.com/item?id=39530435
      https://news.ycombinator.com/item?id=39786142
      https://news.ycombinator.com/item?id=39721626
    • ฟังก์ชันที่มีสี ซึ่ง async/await สร้างขึ้น ยังเพิ่มต้นทุนในการพัฒนาและบำรุงรักษาซอฟต์แวร์ด้วย
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
      หากไม่ใช่ซอฟต์แวร์ที่ต้องการความสามารถในการขยายสูงมาก การประนีประนอมของ async อาจไม่คุ้มค่า
  • แก่นของข้อถกเถียงระหว่าง async/await กับเธรดไม่ใช่ว่าฝั่งไหนซับซ้อนกว่า แต่คือการที่มัน แบ่ง ecosystem ออกเป็นสองส่วน และทำให้ฝั่งหนึ่งกลายเป็นพลเมืองชั้นสอง หากเลือกผิดในโปรเจกต์ก็จะเกิดแรงเสียดทาน
    ถึงจะใช้ปนกันได้ แต่เมื่อต้องทำจริงมักแฮ็กและไม่มีประสิทธิภาพ ตอนนี้ ecosystem ของ Rust ถูกตัดสินไปแล้วว่า หากมี I/O เข้ามาเกี่ยวข้อง แทบทั้งหมดจะถูกผูกกับ ecosystem ของ async/await และแทบทุกสิ่งที่อยากทำใน Rust มักมี I/O รวมอยู่ด้วย ยกเว้นกรณีที่พบไม่บ่อย ดังนั้นไม่ว่าส่วนที่เหลือของแอปพลิเคชันจะต้องการ async หรือไม่ ไลบรารีที่ไม่ใช่ async ก็มักต้องถูกมองข้าม
    ถ้า Rust ใช้ abstraction ที่ประกอบกันได้ดีกว่า async/await และความสามารถในการประกอบนั้นไม่บังคับให้สิ่งอื่น ๆ ต้องกลายเป็น async/await ไปด้วย ความไม่พอใจส่วนใหญ่ก็น่าจะหายไป

    • เห็นด้วยกับการวินิจฉัยนี้ ในบทความ Rust async ของผม[0] ก็ได้ข้อสรุปเดียวกัน
      ที่แย่กว่านั้นคือ ecosystem ไม่ได้แค่แยกเป็นสองฝั่ง แต่แม้ในโค้ด async เองก็มักผูกกับ executor หนึ่ง ๆ อย่างแน่นหนา โดยมากคือ Tokio ถ้าขยายปัญหาเรื่องสีของฟังก์ชัน จากสีน้ำเงิน (ไม่ใช่ I/O), สีเขียว (blocking I/O), สีแดง (async I/O) ในความเป็นจริงมันกลายเป็นสีน้ำเงิน, สีเขียว, สีแดง (Tokio), สีม่วง (async-std), สีส้ม (smol)
      ผมมองว่า sans-I/O pattern เป็นทางออกที่ดีที่สุดสำหรับปัญหานี้ แยกโค้ดสีน้ำเงินทั้งหมดออกมา และใช้ inversion of control กับ I/O และเวลา ก็จะทำให้ logic หลักของ protocol ไม่ต้องรู้เรื่อง I/O และห่อด้วย I/O หลายรูปแบบได้ง่าย
      0: https://hugotunius.se/2024/03/08/on-async-rust.html
    • ควรมองข้ามไลบรารีที่ไม่ใช่ async ก็ต่อเมื่อมีไลบรารีให้เลือกสองตัวและอย่างอื่นเหมือนกันเท่านั้น กรณีแบบนั้นพบได้น้อย
      การใช้โค้ด blocking ในแอปพลิเคชัน async ไม่ได้ลื่นไหลอย่างที่คาด แต่ก็ไม่ได้ยาก แทนที่จะใช้ foo() ก็ใช้ tokio::spawn_blocking(foo).await ซึ่งจะรันโค้ดใหม่ในเธรดแยก และคืน future ที่เสร็จเมื่อเธรดนั้นจบงาน
    • C# ก็คล้ายกันโดยพื้นฐาน
      ถึง I/O จะมีตัวเลือกที่ไม่ใช่ async อยู่บ้าง แต่ถ้าใช้ตัวเลือก async แล้ว โดยพฤตินัยจะถูกบังคับให้ทุกอย่างเป็น async ไปจนถึง Main() มีวิธีเรียกเมธอด async จากเมธอด synchronous ได้อย่างปลอดภัยเหมือนกัน แต่มันทำให้การดีบักยากขึ้นอย่างสุดขีด
  • ในบทความมีหลายส่วนที่ตกหล่นไป
    async/await ทำงานในบริบทของเธรดเดียว จึงไม่ต้องใช้ lock หรือ synchronization แต่ถ้ารัน async/await บนหลายเธรดเพื่อใช้ประโยชน์จากคอร์ CPU ก็จะต้องใช้ lock และ synchronization อีกครั้ง ความซับซ้อนนี้อาจถูกซ่อนไว้ในโค้ดภายนอกได้ เช่น แทนที่จะ synchronize การเข้าถึง DB connection เดียว การเปิด DB connection หนึ่งตัวต่อ async task อาจง่ายกว่า แต่ใน SQLite หรือ PostgreSQL อาจกระทบต่อประสิทธิภาพ
    ใน async/await การ ส่งต่อข้อผิดพลาด ไม่ชัดเจน โดยเฉพาะเมื่อพยายามรวม async task เข้าด้วยกัน และ Happy Eyeballs เป็นตัวอย่างแบบคลาสสิก
    ถ้าพูดถึง network I/O ก็ต้องพูดถึง backpressure ด้วย การนำ async/await ไปใช้ใน CPython ขึ้นชื่อว่า backpressure ของเครือข่ายไม่เพียงพอ และมีปัญหาที่เกิดจากเรื่องนั้น

    • async/await มีปัญหาหลายอย่าง แต่ข้อไม่พอใจที่ใหญ่ที่สุดคืออันนี้
      “Design Patterns” ของ Gang of Four ส่วนใหญ่เป็นตำราสูตรสำเร็จสำหรับเลี่ยงข้อบกพร่องของ C++ แต่ผู้คนกลับนำ pattern เหล่านั้นไปใช้กับภาษาที่ไม่มีข้อบกพร่องแบบนั้นด้วย
      Rust ไม่ใช่ JavaScript และสามารถรันหลายเธรดได้ดี ดังนั้นจึงไม่จำเป็นต้องใช้ async/await เสมอไป และในฐานะภาษาเชิงระบบ มันสามารถลองแนวทางอื่น ๆ ได้
      แต่ถ้าจะผลัก Rust ให้กับโปรแกรมเมอร์ JavaScript ก็จำเป็นต้องมี async/await without.boats เขียนไว้ว่า “ผลักดัน async/await ด้วยความกระตือรือร้นอย่างจริงใจ ภายใต้สมมติฐานว่าความอยู่รอดของ Rust ขึ้นอยู่กับฟีเจอร์นี้”
      https://without.boats/blog/why-async-rust/
      ดูเหมือนว่าการที่ async/await เข้ากับ Rust ทางเทคนิคได้ดีหรือไม่ไม่ใช่เรื่องสำคัญ แต่เพราะโปรแกรมเมอร์ JavaScript คุ้นกับ async/await ดังนั้น Rust ก็ต้องมี async/await ด้วย
    • async/await ก็เป็น กลไก concurrency เช่นเดียวกับเธรด และเมื่อต้องเข้าถึง shared memory ก็ต้องมี lock เสมอ ไม่รู้ว่าคำพูดที่ว่าไม่ต้องใช้ lock มาจากไหน
    • async อาจน่ากลัวกว่าในแง่ของ lock
      ถ้าโค้ดบล็อกหนึ่งเคยพึ่งพาการเข้าถึงแบบ exclusive และเพราะไม่มี await จึงรับประกันสิ่งนั้นได้ แต่พอเพิ่ม await เข้าไประหว่างกลาง โค้ดก็พัง Threading อย่างน้อยยังบังคับให้ระบุในโค้ดว่าอะไรต้องการการเข้าถึงแบบ exclusive
      async ยังหมายถึงการจัดการ scheduling ของเธรดเองด้วย ถ้า I/O เยอะและโค้ดที่ CPU-bound สั้นก็ไม่เป็นไร แต่ถ้ามีโค้ด CPU-bound หรือแม้แต่นาน ๆ ครั้ง ก็จะกลายเป็นการเล่นบท scheduler
    • ตอนเข้าร่วมทีมที่ใช้ Node.js ผมเจอปัญหา backpressure
      หลาย service ตายแบบ ABEND ไปเฉย ๆ และจากมุมคนที่มาจาก Java การตกหล่นแบบนี้น่าประหลาดใจมาก อธิบายวิธีแก้ให้ทีมเข้าใจก็ยาก
      เพราะเรื่อง การส่งต่อข้อผิดพลาด ถ้ามีทางเลือก ผมจะไม่ใช้ async/await ถ้าเป็นโปรเจกต์ทำคนเดียวก็อาจจะได้ แต่ถ้าทำร่วมกับคนอื่น ใช้ไลบรารี และต้องทำให้ทุกคนเข้าใจตรงกัน ผมไม่เอาเด็ดขาด
      ผมยังไม่ได้ใช้ structured concurrency ระดับภาษาอย่างจริงจัง แต่กำลังฝากความหวังไว้กับ Project Loom ของ Java ดูแล้วน่าจะทำให้ข้อถกเถียงนี้หมดความหมายไป
    • ใน Rust, async/await ไม่ได้รันอยู่แค่ในบริบทของเธรดเดียว
  • มีปัญหาในบทความ
    ตัวอย่างมีแค่เว็บเซิร์ฟเวอร์เดียว และฝั่งวิธีแก้ด้วยเธรดก็แก้ผิด อีกอย่าง คำถามดูเหมือนตั้งสมมติฐานว่าผู้คนต้องการเธรดของ OS แทน async/await
    สิ่งที่โปรแกรมเมอร์ต้องการคือเธรดในเชิงแนวคิดและเชิงความหมาย คือการเขียนตรรกะแบบลำดับ และไม่ต้องเขียนคำกำกับแปลก ๆ อย่าง async ถ้า async/await ดีขนาดนั้น ทำไมไม่ทำให้ทุกฟังก์ชันเป็น async โดยปริยาย แล้วใช้การเรียกฟังก์ชันปกติแทน await ไปเลยล่ะ แบบนั้นก็เท่ากับเขียนโปรแกรมด้วยเธรดอยู่ดี
    เธรดของ OS แพงเพราะมีสแตกที่จัดสรรแบบคงที่ และสิ่งที่เราต้องการคือ เธรดราคาถูก ที่รันได้เป็นล้าน ๆ ตัวบน CPU ตัวเดียว เพียงแต่ไม่ต้องมีคำหยาบ ๆ อย่าง async/await เท่านั้น wait อาจคงไว้สำหรับการรอแบบบล็อกในความหมายคลาสสิก เช่น รออีเวนต์หรือรอเธรดอื่นจบ แต่ไม่อยากให้ใช้กับการเรียกฟังก์ชัน
    กลับไปที่ตัวอย่างเว็บเซิร์ฟเวอร์ ถ้าใช้ driver.race(timeout).await เพื่อทำ timeout แล้วหลังจาก race แจ้งข้อผิดพลาด timeout ซ็อกเก็ตของไคลเอนต์จะเป็นอย่างไร มันจะไม่รั่วไหลโดยยังเปิดและเชื่อมต่อค้างอยู่หรือ
    timeout ในเวอร์ชันเธรดก็ทำให้เกือบเหมือน async/await ได้ เช่น threaded_race(client_thread, timeout).wait โดย threaded_race จะใช้ตัวจับเวลาติดตาม timeout ควบคู่ไปกับเธรด และเมื่อถึงเวลาก็เรียก client_thread.interrupt() แบบ Java Thread.interrupt() ถ้าเธรดไม่ได้ถูกบล็อกอยู่ก็แค่ตั้งแฟล็กไว้ และถ้าถูกบล็อกอยู่ในการเรียก I/O ก็จะโยน InterruptedException เนื่องจากเป็น checked exception คอมไพเลอร์จึงบังคับให้ครอบ client.read_to_end(&mut data) ด้วย try/catch หรือประกาศ exception ไว้ใน handle_client ทำให้โปรแกรมเมอร์ไม่ลืมปิดซ็อกเก็ตของไคลเอนต์

    • ค่าภายใน race() จะถูก Drop และตัว driver เองยังคงอยู่
      ถ้าใช้ชนิดข้อมูลตามนั้น Rust จะบ่นว่าไม่ได้จัดการ Result และถ้าซ็อกเก็ตใหม่ถูกสร้างเป็น local ภายใน future มันก็จะถูกเก็บกวาด
      ข้อดีของ Rust future คือสามารถกำหนดพฤติกรรมรอบ ๆ มันทั้งหมดได้ ต่างจากโมเดลที่ทุกฟังก์ชันเป็นแบบบล็อก Rust สามารถระบุจุดที่จะเลื่อนการทำงานไปเป็นงานถัดไปในคิวงานได้ และมีสถานะที่เก็บไว้อย่างชัดเจน (struct Future) เพื่อ poll งานได้เร็วตามต้องการ ดังนั้นจึงไม่ต้อง sleep() เพื่อ yield เหมือนเธรด ทำให้เร็วและให้เหตุผลได้ง่ายกว่า
      Thread.interrupt ของ Java สุดท้ายก็ใกล้เคียงกับลูป sleep และอาจโอเคสำหรับแอปพลิเคชันส่วนใหญ่ แต่ Rust เป็นภาษาเชิงระบบ จึงปล่อยให้ใช้วิธีแบบนั้นในระบบฝังตัวไม่ได้ และก็ไม่เหมาะกับเคอร์เนลหรือแอปพลิเคชันที่ต้องการ latency ต่ำด้วย
    • โปรแกรมเมอร์บางคนต้องการตรรกะแบบลำดับ แต่หลายคนต้องการตรงกันข้าม
      ในกรณีส่วนใหญ่ ผมไม่ค่อยสนใจนักว่ามันเป็น system call แบบบล็อกของ OS หรือ system call แบบไม่บล็อก แต่ผมอยากเข้าใจ control flow ของโปรแกรมที่กำลังอ่านอยู่ รู้ว่ามันรอตรงไหน และจะรันขนานกันได้อย่างไร
      กลับกัน เวลาทำงานกับฟังก์ชันแบบบล็อก ผมอยากให้มีคู่คีย์เวิร์ด blocking/block ด้วยซ้ำ การเรียกแบบบล็อกอาจทำให้ทั้งระบบช้าลงโดยไม่รู้ตัว และผมเห็นแอปที่หน่วงจนน่าหงุดหงิดเพราะมี system call แบบบล็อกอยู่ใน UI thread มามากเกินไปแล้ว
    • แนวทางที่ทำให้ทุกฟังก์ชันเป็น async โดยปริยายแล้วใช้เหมือนการเรียกฟังก์ชันปกติ ถูกลองมาหลายครั้งตลอดหลายสิบปีที่ผ่านมา ลองค้นคำว่า “RPC” ดูได้
      ความพยายามรวม synchronous กับ asynchronous เข้าด้วยกันล้วนล้มเหลว โค้ดที่รันในเธรดเดียว โค้ดที่รันระหว่างเธรด และแม้แต่โค้ดที่รันระหว่างคอมพิวเตอร์ มี ความแตกต่างเชิงความหมาย อย่างมาก ถ้าพยายามทำ abstraction กลบมัน สุดท้ายก็จะไม่พอ ดังนั้นเรียนให้ถูกตั้งแต่แรกดีกว่า
    • จำได้ว่า withoutboats เคยเขียนในบทความหนึ่งว่าคำตอบที่แท้จริงคือ ความเข้ากันได้กับ C
    • ยังมีวิธีเขียนโค้ดด้วย poll() และ select() ด้วย แต่นั่นก็เป็นอีกแนวทางหนึ่งต่างหาก
  • น่าสนใจที่ได้เห็นแคมเปญแทบจะเหมือนการตลาดเพื่อกู้หน้าให้ async/await
    จากประสบการณ์ของผม นี่ไม่ใช่แค่ความผิดพลาดทางเทคนิค แต่ยังทำให้ชุมชนต้องจ่ายต้นทุนสูงด้วย แทนที่จะโฟกัสกับฟีเจอร์ภาษาที่มีประโยชน์จริง ๆ ความพยายามของ Rust กลับถูกเบี่ยงไปกับความสับสนนี้
    ถึงอย่างนั้นผมก็ยังคาดหวังกับภาษานี้มาก และคิดว่าในบรรดาสิ่งที่มีอยู่ตอนนี้มันดีที่สุด เพียงแต่กังวลว่าศึกนี้จะยืดเยื้อไปตลอดกาล
    ป.ล. ตัวอย่าง AsyncWrite/AsyncRead ดูน่าเชื่อถือ แต่จริง ๆ ถ้าจำกัดไว้ที่ *nix ก็ทำแบบเดียวกันด้วยเธรดและ file descriptor ได้

    • ผมเคยใช้ async ในเฟิร์มแวร์ และมันช่วยชีวิตไว้เลย
      การเหมารวมแบบนั้นดูมีหลักฐานไม่พอ และเหมือนจะเอนเอียงไปตาม workload เฉพาะบางแบบ
    • ผมไม่ค่อยรู้ Rust ดี จึงไม่แน่ใจว่าถูกไหม แต่จากประสบการณ์ เหมือนว่าการถกเรื่อง Rust 9 ใน 10 ครั้งที่เห็นบน HN/reddit ช่วงนี้วนอยู่กับ async
      สำหรับคนที่ไม่สนใจ async เลย แต่อยากอ่านเรื่อง Rust มันค่อนข้างแย่
    • ถ้าคิดว่าเธรดเร็วกว่า poll() ผมอยากรู้ว่าเป็น use case แบบไหน ทั้งชีวิตยังไม่เคยเห็นกรณีแบบนั้นเลย
    • มันไม่ใช่ความผิดพลาดทางเทคนิค แต่เป็นวิธีแก้ที่ยอดเยี่ยมเมื่อจำเป็นต้องใช้ โค้ด async ที่ latency ต่ำมาก
      ความผิดพลาดคือการผลักมันไปถึงกรณีใช้งานส่วนใหญ่ที่ไม่ได้ต้องการสิ่งนั้น
    • สงสัยว่ามีหลักฐานไหมที่บอกว่างาน async แย่งฟีเจอร์ที่มีประโยชน์อื่น ๆ ไป
      โปรเจกต์ Rust สำคัญ ๆ จำนวนมากพึ่งพา async ไม่ใช่แค่เพราะประสิทธิภาพดีขึ้นมากเมื่อเทียบกับทางเลือกแบบใช้เธรด แต่ยังเพราะคุณลักษณะด้านการออกแบบด้วย ใน workload หลัก ๆ ที่ติด I/O จะเห็นข้อดีเหล่านี้ได้ง่าย การที่คนฉลาด ๆ ที่แก้ปัญหาจริงนำ async ไปใช้กันอย่างกว้างขวางใน crate หลัก ๆ เป็นสัญญาณที่แรงว่า async เป็นฟีเจอร์ภาษาที่มีประโยชน์จริง
      การทะเลาะกันส่วนใหญ่อยู่บน Hacker News กับ reddit ในรูปแบบที่คนซึ่งไม่ต้องการ async โกรธเพราะ crate I/O ที่พวกเขาใช้ตอนนี้ต้องการ async แล้ว ผมเข้าใจว่ามันเป็นสถานการณ์ที่ไม่น่าสนุก และ async ก็มีปัญหาจริง ๆ อีกทั้งยังอยู่ระหว่างแก้ไขจริง ไม่ได้สมบูรณ์แบบ แต่ผมรู้สึกว่าความแตกแยกเรื่อง async ที่เห็นในฟอรัมไม่ได้กว้างหรือดราม่าเท่ากับในโปรเจกต์จริง
  • สิ่งที่ขาดไปอย่างมากคือ การยกเลิก
    future นั้นยกเลิกได้ง่ายมาก ในทางกลับกัน การยกเลิกเธรดนั้นยุ่งเหยิงเหมือนเกมตีตัวตุ่น และการบังคับหยุดเธรดก็ไม่น่าเชื่อถือ เพราะมีความเสี่ยงที่ล็อกจะยังค้างอยู่ในสถานะถูกล็อก
    ในโมเดล async ของ Rust สามารถผูก timeout จากภายนอกเข้ากับ future ทุกตัวได้ ไม่จำเป็นที่ฟังก์ชัน I/O ปลายทางทุกตัวต้องรองรับตัวเลือก timeout และไม่จำเป็นต้องส่ง timeout นั้นต่อไปตลอด call stack
    หากใช้ Drop guard ซึ่งเป็นแนวปฏิบัติที่ดีของ Rust สำหรับการจัดการสถานะที่กำลังดำเนินอยู่ร่วมด้วย ก็จะยกเลิกงานขนาดใหญ่และซับซ้อนได้อย่างง่ายดายและเชื่อถือได้

    • ไม่ใช่ว่า future ทุกตัวจะยกเลิกได้ง่าย แต่เป็นการทำให้ดูเหมือนว่ายกเลิก future ทุกตัวได้ง่ายต่างหาก
      ตัวอย่างเช่น แม้จะยกเลิก (drop) บางสิ่งที่ใช้ spawn_blocking มันก็ยังทำงานต่อในเบื้องหลัง และผู้ใช้อาจไม่รู้ การทำงานกับไฟล์ระบบแบบ async ที่ implement ด้วย thread pool ก็เช่นกัน แม้จะยกเลิกแล้วก็ยังทำงานต่อ
      สิ่งแบบนี้อาจนำไปสู่บั๊กที่เข้าใจยาก เช่น “มั่นใจว่าไม่มีอะไรเขียนไฟล์นั้นอยู่ แล้วทำไมบริการถึงล้มเหลวเพราะไฟล์กำลังถูกใช้งาน?”
    • ถ้า implement async future ได้ ก็ย่อม implement เธรดที่ยกเลิกได้ แทนได้เช่นกัน
      ปัญหานี้ค่อนข้างมีรูปแบบเทียบเคียงกันได้ system call นั้นยากก็จริง แต่ไม่ว่าจะทำในเธรดหรือใน async future หากเรียก system call เดียวกัน ก็จะเจอปัญหาการยกเลิกแบบเดียวกันเป๊ะ
    • ไม่เข้าใจว่าทำไมการยกเลิกเธรดถึงยาก
      แค่มีสถานะอย่าง flag ที่ทุกเธรดเข้าถึงได้ แล้วให้ลูปการทำงานตรวจ flag นั้น ถ้าเป็น false ก็ return แล้ว join เธรดก็จบ
    • จากประสบการณ์ของผม การยกเลิกไม่ใช่เรื่องที่คุ้มจะกังวลมากนัก
      ถ้างานบางอย่างไม่มีประโยชน์อีกต่อไป ก็เพียงพอแล้วถ้าข้อมูลนั้นสุดท้ายไปปรากฏให้ฟังก์ชันที่ถูกเรียกแทนงานนั้นเห็น ถ้าไม่ใช่จังหวะก่อนทำงานที่แพงมากอย่างการเริ่ม RPC ก็ไม่จำเป็นต้องตรวจเป็นพิเศษ
  • คำถามที่ดีกว่าน่าจะเป็น “ทำไมถึงเป็น async/await ไม่ใช่ fiber?”
    ผมรู้ว่า Rust เคยมี green thread ก่อนเวอร์ชัน 1.0 และจงใจเอาออก แต่การทำ concurrency แบบอิง fiber ก็มีได้หลายแนวทาง เช่น แบบที่ไม่ต้องฝัง runtime หนัก ๆ ไว้ในภาษา
    ถ้าผมเข้าใจบทความถูก ประเด็นที่ชมเป็นหลักดูเหมือนจะเป็นเรื่องที่สามารถ drop future เมื่อไรก็ได้ ใน thread นั้นทำแบบเดียวกันไม่ได้ด้วยเหตุผลที่ชัดเจน และแม้ในทางเทคนิคจะทำได้ก็ไม่ปลอดภัยอย่างยิ่ง แต่ความสามารถนี้มีต้นทุนมหาศาลตามมา ไม่ใช่แค่ว่าไม่สามารถใช้ executor แบบ completion-based อย่าง io-uring ร่วมกับอาร์เรย์บนสแตก หรือให้ subtask ไปรันบน thread executor อื่นได้เท่านั้น แต่ยังทำให้เกิดกับดักปลีกย่อยและปัญหาความน่าเชื่อถือที่กลายเป็นเรื่องน่าประหลาดใจอย่างไม่พึงประสงค์มาก ๆ เมื่อเขียน Rust แบบ synchronous
    https://smallcultfollowing.com/babysteps/blog/2022/06/13/asy...
    ผมมองว่าการยกเลิกงานโดยพื้นฐานแล้วต้องเป็น cooperative cancellation และ non-cooperative cancellation นั้นแม้ผิวเผินจะสะดวก แต่ค่อนข้างเป็นฟีเจอร์ที่ผิดพลาดซึ่งมีปัญหาลึก ๆ ซ่อนอยู่ข้างใต้
    อีกอย่าง การชื่นชม composability ของ async/await ก็ดูแปลก ใน Rust ปัจจุบันที่ไม่มี proper effect system มันห่างไกลจากการ compose ได้ เพราะมีลักษณะ “แพร่เชื้อ” ลองใช้เมธอด map ของ standard library ร่วมกับ async closure หรือใช้ trait มาตรฐาน io::Read/Write ดูก็ได้

    • ถ้าจะใส่ fiber เข้าไปใน Rust ในฐานะ abstraction สำหรับ cooperative concurrency ใน user space ก็จะบังคับให้ต้องตัดสินใจเรื่องการออกแบบหลายอย่าง
      ต้องเลือกว่าจะทำสแตกเป็น spaghetti stack, บังคับใช้ไลบรารี memory mapping ระดับ process, หรือจำกัดให้เป็นสแตกขนาดคงที่
      ทั้งสามแบบมีปัญหาเมื่อโต้ตอบกับโค้ดภาษาที่มี ABI ต่างกัน ตัวอย่างเช่น ถ้า fiber หนึ่งเรียกโค้ด C แล้วโค้ด C นั้นพยายาม resume fiber อื่น เรื่องอาจซับซ้อนขึ้นมาก
      ข้อดีอย่างหนึ่งของ async/await คือคีย์เวิร์ด await เอง จุดรอที่ประกาศชัดเจนทำให้เราสามารถให้เหตุผลเกี่ยวกับปฏิสัมพันธ์ของโปรแกรม concurrent ได้จริง
      fiber ที่ yield ได้คล้ายกับ goto ในโลกของ concurrency เวลาเรียกเมธอดหนึ่ง เราไม่รู้ว่าการประมวลผลจะหยุดลงเป็น side effect หรือไม่ และเมื่อกลับมาทำต่อ สถานะของโลกจะเปลี่ยนไปแล้วหรือเปล่า เมื่อแตะโลกภายนอกจึงต้องเขียนโค้ดแบบ defensive ดังนั้น fiber จึงเหมาะกับงานที่รันแบบโดดเดี่ยวและสื่อสารกันด้วย completion มากกว่า
      green thread, fiber และ coroutine ต่างก็มีปัญหาร่วมกันตรงนี้ cooperative concurrency ใน user space ไม่ได้แก้ส่วนยากของ concurrency เท่าไรนัก แต่เหมือนแค่ย้ายกองเอกสารบนโต๊ะมากกว่า Rust async/await มีความชัดเจนกว่า จึงไม่ซ่อน side effect ที่กลไกอื่นซ่อนไว้
    • ผมไม่แน่ใจว่า fiber แก้ปัญหาการยกเลิกอย่างไร แทบจะเทียบเท่ากันไม่ใช่หรือ?
      โค้ดแบบ fiber รู้สึกตามยาก เพราะต้องติดตาม thread ที่กำลังดำเนินอยู่ในหัว อย่างน้อยสำหรับผม การติดตามค่าที่จะ complete นั้นง่ายกว่ามาก
    • fiber ที่มีสแตก ไม่ค่อยเหมาะกับโค้ดระดับต่ำ
      ดูรีวิวของ Gor Nishanov สำหรับคณะกรรมการ C++ ได้ที่ http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136... ซึ่งถูกลิงก์จาก https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=10... ด้วย สรุปก็ชัดเจน: DO NOT USE FIBERS!
    • เพราะเป็น Ruby สถานการณ์เรื่อง thread/GVL จึงต่างจาก Rust แต่ผมสงสัยว่าหมายถึงเรื่องแบบนี้หรือเปล่า
      https://m.youtube.com/watch?v=qKQcUDEo-ZI
      ผมคิดว่ามันแสดงเหตุผลได้ค่อนข้างดีว่า async/await นั้นแพร่เชื้อและเทอะทะ ขณะที่ fiber อย่างน้อยใน implementation ของ Ruby เป็น paradigm ที่ดีกว่ามาก
    • ในปัญหาคณิตศาสตร์ cooperative cancellation อาจน่ารำคาญพอสมควร
      optimization algorithm อาจเรียกปัญหา root finding แล้วสิ่งนั้นก็เรียก ODE integrator ซ้อนกันไป โดยแต่ละขั้นอาจรันนานมาก ต้องส่ง cancellation token ไปทั่วทุกที่ แต่ framework สำหรับ numerical computation โดยทั่วไปไม่รองรับ
      เราสามารถและควรกำหนดขีดจำกัดจำนวนรอบให้ทุกอัลกอริทึมได้ แต่ในอัลกอริทึมที่ซ้อนกัน มันรับประกันได้แค่ว่าจะหยุดภายในปีนี้ ไม่ใช่รับประกันได้ยากว่าจะหยุดภายใน 5 วินาที
      สำหรับปัญหาแบบนี้ ผมรับประกันได้ว่าสิ่งที่ทำมีแค่การคำนวณคณิตศาสตร์จำนวนมาก, การ allocate และ page fault ที่ตามมา, ไม่มี I/O, และเขียนสตริง log ลงในออบเจ็กต์ Queue ของ standard library เพื่อให้ Main thread ที่จะไม่ถูกยกเลิกจัดการเท่านั้น ฟีเจอร์อื่นที่จำเป็นก็สามารถส่งกลับไปให้ main thread ผ่าน Queue ได้
      ผมรู้สึกว่าในศตวรรษที่ 21 ปัญหานี้ควรแก้ได้โดยไม่ต้องยัด cancellation token ไปทุกที่ และไม่ต้องบังคับให้เขียนโค้ดที่รันนานแบบ defensive เพียงเพราะมันไม่ตรวจ token
  • ในอีกการถกเถียงเรื่อง async/await ผู้คนไม่เข้าใจ async/await, จินตนาการไม่ออกว่าทำไมจึงต้องมีกลไก concurrency บน thread เดียว และสมมติว่าไม่มีใครต้องการมัน
    การเขียนโปรแกรม UI, การสื่อสารกับ GPU, และการสื่อสารระหว่าง runtime เป็นตัวอย่างที่ดี และน่าจะมีอย่างอื่นอีก
    thread ไม่ว่าจะเป็น green thread หรือไม่ ไม่เหมาะกับกรณีเหล่านี้ แต่ async/await เหมาะ

    • ใน GUI สามารถใช้ thread ได้ง่าย และในอดีตผมเคยเขียนแอป GUI หลายตัวที่ใช้ thread ได้ค่อนข้างมีประสิทธิภาพ
    • ความสามารถในการจัดการงานหลายงานอย่างชัดเจนบน thread เดียวนั้นสำคัญแน่นอน
      ถ้าสามารถ implement ฟีเจอร์ภาษาอื่นที่สร้างไบนารีเดียวกันแต่ทำให้ผู้ใช้ภาษาลำบากน้อยกว่าได้ ก็เป็นเรื่องที่ควรคุยกัน
  • ข้อดีสำคัญอย่างหนึ่งของ async/await ใน Rust คือสามารถทำงานได้แม้ในสถานการณ์ที่ ไม่มีเธรดหรือหน่วยความจำแบบไดนามิก
    มันยังใช้เขียนโค้ดที่กระชับบนไมโครคอนโทรลเลอร์ได้ดีพอ เช่น รอให้อินเทอร์รัพท์อ่านข้อมูล I2C ที่เข้ามาในบัฟเฟอร์ใดบัฟเฟอร์หนึ่ง เป็น abstraction ระดับสูงกว่าที่ช่วยให้ใช้ concurrency ได้โดยไม่ต้องเปิดเผยการโต้ตอบกับ runtime ชั้นล่างมากนัก
    ซอฟต์แวร์หลักทั้งหมดที่ผมเคยทำงานด้วยล้วน implement สิ่งทำนองนี้ในรูปแบบใดรูปแบบหนึ่ง แม้ในโค้ดที่ไม่มีแนวคิด coroutine แบบ C++ สมัยใหม่ ก็ยังใช้ Apple Grand Central Dispatch, Intel Threading Building Blocks ฯลฯ ไม่อย่างนั้น business logic จะไปติดค้างกับ I/O อย่างไร้ประสิทธิภาพมาก หรือมีเธรดมหาศาลจนการพัฒนาและดีบักกลายเป็นนรก หรือถูกกลบด้วยรายละเอียดการ implement ของ runtime ชั้นล่าง หรือปนกันทั้งสามอย่าง
    ถ้าไม่ใช้ abstraction ที่มีอยู่แล้วในตัวภาษาเองหรือในไลบรารี สุดท้ายก็ต้องสร้างเอง ซึ่งยากและโดยรวมมีแนวโน้มจะด้อยกว่าสิ่งที่ใช้กันแพร่หลาย ผมเองก็เคยทำขึ้นเองสำหรับ C++ มาก่อน: https://github.com/goto-opensource/asyncly

  • ดูเหมือนผู้เขียนจะสับสนสองเรื่องเข้าด้วยกัน
    เรื่องหนึ่งคือ เธรดใน user space/green thread และอีกเรื่องคือ structured concurrency
    อย่างแรกเป็นข้อดีของ async/await แต่ไม่ใช่ข้อดีเฉพาะตัว มีตัวอย่างที่ทำได้โดยไม่มีปัญหาเรื่องสีของฟังก์ชัน เช่น Go หรือ Java Loom
    อย่างหลังสามารถ implement ได้ทั้งด้วยเธรดของ OS และ green thread ดู JEP เรื่อง Structured Concurrency ของ Java ได้
    https://openjdk.org/jeps/462