4 คะแนน โดย GN⁺ 2024-03-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในชุมชน Rust มักจะเห็นคำถามบ่อย ๆ ว่า ถ้าเธรดทำทุกอย่างที่ async/await ทำได้ แถมยังง่ายกว่า แล้วทำไมถึงต้องเลือก async/await?
  • Rust เป็นภาษาแบบ low-level และไม่ได้ซ่อนความซับซ้อนของ coroutine เอาไว้ ซึ่งเป็นแนวคิดตรงข้ามกับภาษาอย่าง Go ที่ทำให้ทุกอย่างเป็นอะซิงโครนัสโดยปริยายจนโปรแกรมเมอร์แทบไม่ต้องคิดเรื่องอะซิงก์
  • โปรแกรมเมอร์ที่ฉลาดมักพยายามหลีกเลี่ยงความซับซ้อน แล้วเหตุใด async/await จึงยังจำเป็น?

ทำความเข้าใจพื้นหลัง

  • Rust เป็นภาษาแบบ low-level โค้ดจึงมักทำงานเป็นเส้นตรง โดยเมื่อหนึ่งงานเสร็จแล้วจึงค่อยทำอีกงานหนึ่ง
  • แต่เมื่อจำเป็นต้องรันหลายงานพร้อมกัน เช่น ในเว็บเซิร์ฟเวอร์ โค้ดแบบเส้นตรงจะเริ่มมีปัญหา
  • เว็บยุคแรก ๆ พยายามแก้ปัญหานี้ด้วยการนำเธรดมาใช้
  • แม้จะใช้เธรดเพื่อรองรับไคลเอนต์หลายรายพร้อมกันได้ แต่โปรแกรมเมอร์ก็ต้องการย้ายความสามารถด้าน concurrency จากพื้นที่ของ OS มาอยู่ใน user space

ปัญหาเรื่อง timeout

  • จุดแข็งที่สุดอย่างหนึ่งของ Rust คือ composability
  • async/await ทำให้สามารถนำ composability นี้มาใช้กับฟังก์ชันที่เป็น I/O-bound ได้
  • ตัวอย่างเช่น หากต้องการเพิ่ม timeout ให้กับฟังก์ชันจัดการไคลเอนต์ ก็สามารถทำได้ด้วยการใช้ combinator สองตัว

เธรดเชิงธีม

  • การทำ timeout ในตัวอย่างที่ใช้เธรดนั้นไม่ใช่เรื่องง่าย
  • TcpStream มีฟังก์ชัน set_read_timeout และ set_write_timeout ให้ใช้ แต่แนวทางนี้ก็มีข้อจำกัด
  • บทความนำเสนอวิธีเขียน timeout โดยใช้ combinator ของ Rust แต่ก็ยังจำกัดอยู่กับ TcpStream และต้องมี system call เพิ่มเติม

กรณีความสำเร็จของ Async

  • ระบบนิเวศ HTTP ได้เลือกใช้ async/await เป็นกลไกหลักของ runtime
  • tower เป็นตัวอย่างที่แสดงพลังของ async/await ได้ชัดเจน โดยมีทั้ง timeout, rate limiting และ load balancing
  • macroquad ซึ่งเป็นเอนจินเกมของ Rust ก็รันเอนจินด้วย async/await เช่นกัน

ปรับภาพลักษณ์ของ Async

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

ความเห็นของ GN⁺

  • async/await เพิ่มความซับซ้อนของโค้ดเมื่อต้องจัดการ concurrency แต่ในขณะเดียวกันก็ทำให้สามารถรองรับไคลเอนต์จำนวนมากได้อย่างมีประสิทธิภาพ
  • บทความนี้เน้นว่า async/await ไม่ได้มีดีแค่เรื่องประสิทธิภาพ แต่ยังมีจุดแข็งในฐานะโมเดลการเขียนโปรแกรมด้วย
  • async/await ของ Rust มอบ composability สำหรับงาน I/O หลากหลายรูปแบบ ซึ่งมีประโยชน์อย่างยิ่งในงานอย่างบริการเครือข่ายหรือเว็บเซิร์ฟเวอร์
  • หากมองอย่างวิพากษ์ ความซับซ้อนของ async/await อาจเป็นกำแพงสำหรับนักพัฒนามือใหม่ และจำเป็นต้องมีความพยายามด้านการสอนเพื่อช่วยให้ก้าวข้ามจุดนี้
  • โปรเจ็กต์อื่นที่ให้ความสามารถคล้ายกัน ได้แก่การทำ async/await ของ Node.js และไลบรารี asyncio ของ Python ซึ่งต่างก็มอบพาราไดม์ลักษณะใกล้เคียงกัน
  • เมื่อนำ async/await มาใช้ ควรพิจารณาความซับซ้อนและความสามารถในการบำรุงรักษาของโค้ดร่วมด้วย แต่หากต้องรองรับไคลเอนต์จำนวนมากพร้อมกัน โมเดลนี้ก็ให้ข้อได้เปรียบอย่างมาก

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

 
GN⁺ 2024-03-26
ความคิดเห็นจาก Hacker News
  • Async/await และเธรดเดี่ยว

    • async/await บนเธรดเดี่ยวแบบโมเดลของ JavaScript นั้นเรียบง่ายและเข้าใจได้ดี
    • หากใช้เธรด ก็สามารถให้ CPU หลายตัวช่วยประมวลผลปัญหาได้ และ Rust ก็ช่วยจัดการเรื่อง lock
    • สามารถมีเธรดที่มีลำดับความสำคัญต่างกันได้ และจำเป็นเมื่อมีข้อจำกัดด้านการคำนวณ
    • async/await แบบหลายเธรดมีความซับซ้อน ในส่วนที่มีข้อจำกัดด้านการคำนวณ โมเดลอาจพังทลายได้
    • การคำนวณแบบหลายเธรดใน Rust ทำงานได้ไม่ค่อยดี ปัญหามีเช่น:
      • การพังทลายจากความหนาแน่นของ futex: อาจเป็นปัญหาในตัวจัดสรรหน่วยเก็บข้อมูลบางตัว
      • starvation จาก mutex ที่ไม่ยุติธรรม: Mutex มาตรฐานและช่องของ crossbeam-channel ไม่ยุติธรรม
  • Async/await เทียบกับเธรด

    • คำวิจารณ์ไม่ได้เกี่ยวกับความซับซ้อน แต่เกี่ยวกับการที่การเลือกนี้ทำให้ ecosystem แตกแยกและทำให้อีกทางเลือกหนึ่งด้อยกว่า
    • ecosystem ของ Rust ตัดสินใจว่าหากจะทำงาน I/O ก็ต้องใช้ async/await ทั้งหมด
    • ถ้า Rust ทำให้สิ่งอื่นนอกเหนือจาก async/await กลายเป็น abstraction ที่ประกอบร่วมกันได้มากกว่านี้ ความไม่พอใจก็คงหายไป
  • ปัญหาของบทความ

    • ยกตัวอย่างมาแค่เว็บเซิร์ฟเวอร์ตัวอย่างเดียว และแก้ปัญหาเรื่องเธรดได้ไม่ถูกต้อง
    • โปรแกรมเมอร์ต้องการเธรดในเชิงแนวคิดและความหมาย ไม่ใช่ OS thread
    • OS thread มีต้นทุนสูง และเราต้องการเธรดที่มีต้นทุนต่ำ
    • ปัญหาในการทำ timeout ในตัวอย่างเว็บเซิร์ฟเวอร์
  • ประเด็นที่ไม่ได้พูดถึง

    • async/await รันบนเธรดเดี่ยว จึงไม่ต้องใช้ lock หรือการ synchronization
    • การแพร่กระจายของข้อผิดพลาดใน async/await ไม่ชัดเจน
    • ควรกล่าวถึง backpressure ใน network I/O ด้วย
  • ประเด็นสำคัญเรื่องการยกเลิก

    • งานใด ๆ ในอนาคตสามารถยกเลิกได้ง่าย
    • การยกเลิกในเธรดมีความซับซ้อน และการบังคับหยุดเธรดก็ไม่น่าเชื่อถือ
    • ในโมเดล async ของ Rust สามารถเพิ่ม timeout จากภายนอกให้ futures ทั้งหมดได้
  • แคมเปญคล้ายการตลาดให้กับ async/await

    • async/await เป็นความผิดพลาดทางเทคนิค และสร้างต้นทุนมหาศาลให้ชุมชน
    • Rust ยังเป็นภาษาที่ดีที่สุดอยู่ดี แต่ก็กังวลว่าการถกเถียงนี้จะดำเนินต่อไปตลอดกาล
  • Async/await เทียบกับ fiber

    • ก่อนหน้านี้ Rust เคยมี green thread และถูกถอดออกไปโดยตั้งใจ
    • ความสามารถในการ drop futures ได้ทุกเมื่อนั้นมาพร้อมต้นทุนที่สูงมาก
    • การยกย่องความสามารถในการประกอบร่วมกันของ async/await เป็นเรื่องแปลก
  • ข้อดีหลักของ async/await ใน Rust

    • สามารถทำงานได้แม้ในสถานการณ์ที่ไม่มีเธรดหรือหน่วยความจำแบบไดนามิก
    • สามารถใช้ concurrency เพื่อเขียนโค้ดให้กระชับได้
  • ความเข้าใจผิดเกี่ยวกับ async/await

    • มีคนที่ไม่เข้าใจว่าทำไมจึงต้องมีกลไก concurrency บนเธรดเดี่ยว
    • async/await มีประโยชน์กับการเขียนโปรแกรม UI การสื่อสารกับ GPU และการสื่อสารระหว่าง runtime
  • เหตุผลที่เลือก async/await แทนเธรด

    • async/await สามารถลดการใช้หน่วยความจำของสถานะ client/request/task ได้
    • การบีบอัดสถานะสำคัญต่อประสิทธิภาพในยุคปัจจุบันที่หน่วยความจำช้า
    • async/await และ CPS มีประสิทธิภาพในการลดการใช้หน่วยความจำต่อ client