ในโลกของการพัฒนาซอฟต์แวร์ มีหัวข้อไม่กี่เรื่องที่ก่อให้เกิดการอภิปรายอย่างหลงใหดได้เหมือนกับการเลือกระหว่างการเขียนโปรแกรมแบบ async/await และการเขียนโปรแกรมแบบเธรดดั้งเดิม ในขณะที่บทความทางเทคนิคล่าสุดได้สำรวจผลกระทบด้านประสิทธิภาพของทั้งสองแนวทาง ชุมชนนักพัฒนากำลังอภิปรายอย่างจริงจังถึงคำถามทางสถาปัตยกรรมที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับวิธีที่เราจัดการกับการดำเนินการพร้อมกันในแอปพลิเคชันสมัยใหม่
ความตึงเครียดหลักระหว่างการทำงานพร้อมกันแบบชัดแจ้งและแบบโดยนัย
ความแตกแยกพื้นฐานในการอภิปรายนี้มุ่งเน้นไปที่ว่าผู้พัฒนาควรมีอำนาจควบคุมการดำเนินการพร้อมกันมากน้อยเพียงใด Async/await ทำให้การทำงานพร้อมกันชัดเจน - ทุกจุดที่หยุดทำงานจะถูกทำเครื่องหมายด้วยคำสำคัญ await สร้างสิ่งที่บางคนเรียกว่าฟังก์ชันสี (colored functions) ซึ่งทำงานแตกต่างจากโค้ดแบบประสานเวลา (synchronous) ความชัดแจ้งนี้มาพร้อมกับภาระทางความคิด แต่ให้ผู้พัฒนาควบคุมอย่างละเอียดได้ว่าเมื่อใดที่การดำเนินการควรส่งมอบการควบคุม
การใช้งาน Green thread เช่นใน goroutines ของ Go หรือภาษาใน BEAM (Erlang, Elixir) ใช้แนวทางตรงกันข้าม พวกมันทำให้การทำงานพร้อมกันแทบจะมองไม่เห็นในระดับโค้ด โดยอาศัยระบบรันไทม์ที่ซับซ้อนในการจัดการการจัดเวลาและการสลับบริบท ดังที่ผู้แสดงความคิดเห็นหนึ่งระบุเกี่ยวกับภาษา BEAM: คุณไม่มีสิ่งยุ่งยากอึดอัดระหว่างคำสำคัญที่สงวนไว้และลูปเหตุการณ์ (event loop) หากคุณต้องการให้ส่วนอื่นเกิดขึ้นในภายหลังเนื่องจากเหตุการณ์ คุณเพียงแค่จัดการให้เหตุการณ์ประเภทนั้นถูกส่งมา
ไม่มี callback ไม่มีการระบายสี async มีเพียงเหตุการณ์ วิธีแก้ปัญหาเกี่ยวกับเหตุการณ์คือการย้ำและทำให้ลูปเหตุการณ์ของคุณใช้งานได้ทั่วไปมากขึ้น
การแลกเปลี่ยนด้านประสิทธิภาพที่เกินกว่าการวัดมาตรฐานอย่างง่าย
ในขณะที่การอภิปรายเริ่มแรกมักจะมุ่งเน้นไปที่ปริมาณงานดิบ (throughput) แต่การอภิปรายในชุมชนเผยให้เห็นข้อพิจารณาด้านประสิทธิภาพที่ละเอียดอ่อนยิ่งขึ้น การใช้งาน Async/await โดยทั่วไปจะใช้ state machines ที่บันทึกเฉพาะตัวแปรที่จำเป็นในช่วงที่หยุดทำงาน ทำให้มีประสิทธิภาพด้านหน่วยความจำสำหรับ workloads ที่ถูกจำกัดด้วย I/O (I/O-bound) ในทางตรงกันข้าม Green threads รักษาสแต็กแบบเต็มสำหรับแต่ละงานที่ทำงานพร้อมกัน ซึ่งอาจใช้หน่วยความจำมากกว่าแต่หลีกเลี่ยงการ ฉีกร่าง สแต็ก (stack ripping) ที่จำเป็นโดย state machines แบบ async
บทสนทนาเกี่ยวกับประสิทธิภาพได้พัฒนาขึ้นเพื่อยอมรับว่าทั้งสองแนวทางได้ก้าวข้ามขีดจำกัดดั้งเดิมของพวกเขาแล้ว .NET กำลังพัฒนา Runtime Async เพื่อแทนที่ state machines ที่ชัดแจ้งด้วยกลไกการหยุดทำงานของรันไทม์ (runtime suspension mechanisms) ในขณะที่ Go ได้ปรับปรุงการจัดการสแต็กจาก segmented stacks เป็น stack copying ดังที่นักพัฒนาคนหนึ่งสังเกตเห็น ทั้ง green threads และ async/await มีต้นทุนที่สูงกว่าการเขียนโค้ดแบบเธรดเดี่ยว (single-threaded) อย่างมีนัยสำคัญ แต่ต้นทุนของพวกมันแสดงออกในรูปแบบที่แตกต่างกัน
ความแตกต่างหลักระหว่าง Async/Await และ Green Threads
| ด้าน | Async/Await | Green Threads | 
|---|---|---|
| การควบคุม | ชัดเจน (คีย์เวิร์ด await) | โดยปริยาย (จัดการโดย runtime) | 
| การใช้หน่วยความจำ | State machines (บันทึกเฉพาะตัวแปรที่จำเป็น) | Full stacks ต่อแต่ละ task | 
| ระบบนิเวศ | แพร่หลาย (Python, JS, C, Rust) | เฉพาะภาษา (Go, BEAM) | 
| การกระจาย | ต้องใช้เฟรมเวิร์กเพิ่มเติม | มีในตัว (ภาษา BEAM) | 
| ความยากในการเรียนรู้ | สูงกว่าเนื่องจาก "colored functions" | ง่ายกว่าสำหรับกรณีที่ไม่ซับซ้อน | 
| ลักษณะด้านประสิทธิภาพ | ดีกว่าสำหรับงานที่ติด I/O | ดีกว่าสำหรับรูปแบบ concurrent บางแบบ | 
การแบ่งแยกระบบนิเวศและเครื่องมือ
บางทีข้อพิจารณาที่เป็นจริงที่สุดที่เกิดขึ้นจากการอภิปรายในชุมชนคือความ成熟ของระบบนิเวศรอบๆ แต่ละแนวทาง Async/await ได้กลายเป็นสิ่งที่มีอยู่ทั่วไปใน Python, JavaScript, C# และ Rust สร้างคลังไลบรารีที่เข้ากันได้และรูปแบบที่ยอมรับแล้ว อย่างไรก็ตาม สิ่งนี้มาพร้อมกับปัญหาการระบายสีฟังก์ชัน (function coloring problem) ซึ่งโค้ด async และ synchronous มักจะผสมผสานกันอย่างไม่ราบรื่น
ระบบนิเวศ Green thread โดยเฉพาะภาษาใน BEAM นำเสนอความสามารถในการคำนวณแบบกระจาย (distributed computing) ที่ถูกสร้างขึ้นมาในพื้นฐานของการออกแบบ ดังที่ผู้แสดงความคิดเห็นหนึ่งอธิบาย: BEAM มีไว้สำหรับการคำนวณแบบกระจาย คุณสามารถสร้างกระบวนการใหม่ (spawn new processes) สื่อสารระหว่างกระบวนการ (ซึ่งไม่จำเป็นต้องอยู่บนคอมพิวเตอร์เครื่องเดียวกัน) ส่งข้อมูลทุกชนิดระหว่างพวกเขา รวมถึง closures การกระจายตัวในตัวนี้แสดงถึงข้อได้เปรียบทางสถาปัตยกรรมที่สำคัญสำหรับกรณีการใช้งานบางอย่าง
การใช้งานที่โดดเด่นจำแนกตามภาษา
- Async/Await: asyncio ของ Python, async/await ของ JavaScript, async ของ C, async/await ของ Rust
 - Green Threads: goroutines ของ Go, ภาษาในตระกูล BEAM (processes ของ Erlang/Elixir), Project Loom ของ Java
 - แนวทางแบบผสมผสาน: Runtime Async ที่กำลังจะเปิดตัวของ .NET, ตัวเลือก runtime หลากหลายรูปแบบของ Rust
 
เครื่องมือที่เหมาะสมสำหรับงานที่เหมาะสม
ฉันทามติที่เกิดขึ้นจากการอภิปรายของนักพัฒนาชี้ให้เห็นว่าการเลือกระหว่างกระบวนทัศน์เหล่านี้ขึ้นอยู่กับข้อกำหนดของแอปพลิเคชันเฉพาะเป็นอย่างมาก Async/await ทำงานได้ดีในสภาพแวดล้อมที่มีทรัพยากรจำกัดและการเขียนโปรแกรมระบบ (systems programming) ในขณะที่ green threads ให้หลักการยศาสตร์สำหรับนักพัฒนา (developer ergonomics) ที่เหนือกว่าสำหรับแอปพลิเคชันทางธุรกิจและระบบกระจาย (distributed systems)
นักพัฒนาจำนวนมากกำลังใช้แนวทางแบบไฮบริด โดยใช้รันไทม์ async แบบเธรดเดี่ยว (single-threaded) สำหรับการดำเนินการจัดการ ในขณะที่ใช้โซลูชันแบบหลายเธรด (multi-threaded) สำหรับงานที่สำคัญต่อประสิทธิภาพ ดังที่ผู้ปฏิบัติงานคนหนึ่งแบ่งปัน: ในงานของฉันกับโค้ดฝั่งเซิร์ฟเวอร์ ฉันใช้รันไทม์ async หลายตัว รันไทม์หนึ่งเป็นแบบหลายเธรดและจัดการการรับส่งข้อมูลจริงทั้งหมด อีกรันไทม์หนึ่งเป็นแบบเธรดเดี่ยวและจัดการการดำเนินการจัดการ เช่น การส่งเมตริกและบันทึก (logs)
การอภิปรายยังคงพัฒนาต่อไปเมื่อมีการใช้งานใหม่ๆ เกิดขึ้น ขณะนี้ Rust มีรันไทม์ async เช่น Glommio และ Monoio ที่สร้างบน io_uring ในขณะที่การปรับปรุงที่กำลังจะมาถึงของ .NET สัญญาว่าจะลดช่องว่างประสิทธิภาพระหว่างโค้ด async และ synchronous สิ่งที่ชัดเจนคือทั้งสองแนวทางจะยังคงอยู่ร่วมกันต่อไป โดยแต่ละแนวทางแก้ปัญหาที่แตกต่างกันในภูมิทัศน์ที่ซับซ้อนของการพัฒนาซอฟต์แวร์สมัยใหม่
บทสนทนาเกี่ยวกับ async เทียบกับ threads ไม่ได้เกี่ยวกับว่าแนวทางใดดีกว่าโดยสากลอีกต่อไป แต่เป็นเรื่องเกี่ยวกับการทำความเข้าใจการแลกเปลี่ยนและเลือกเครื่องมือที่เหมาะสมสำหรับกรณีการใช้งานเฉพาะ เมื่อภาษาการเขียนโปรแกรมยังคงพัฒนาต่อไป เรากำลังเห็นการบรรจบกันมากกว่าการแยกออกจากกัน โดยระบบนิเวศแต่ละแห่งยืมแนวคิดที่ประสบความสำเร็จจากระบบนิเวศอื่นๆ
อ้างอิง: [Quite] A Few Words About Async
