3 คะแนน โดย GN⁺ 2025-10-05 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การจัดการ การยกเลิก ในสภาพแวดล้อม Rust แบบอะซิงโครนัส นั้นสะดวก แต่หากจัดการไม่ดีอาจทำให้เกิดบั๊กที่ไม่คาดคิดและปัญหาที่ยุ่งยากได้
  • ใน Rust แบบซิงโครนัส มักต้องตรวจสอบแฟล็กอย่างชัดเจนหรือจบโปรเซส แต่ใน Rust แบบอะซิงโครนัส สามารถยกเลิกได้ง่ายมากเพียงแค่ drop future
  • ความปลอดภัยเมื่อยกเลิก (cancel safety) และความถูกต้องของการยกเลิก (cancel correctness) เป็นคนละแนวคิดกัน โดยการยกเลิก future หนึ่งตัวอาจก่อปัญหาต่อทั้งระบบได้
  • รูปแบบปัญหาหลักที่เกี่ยวกับการยกเลิก ได้แก่ Tokio mutex, แมโคร select, try_join และการใช้งาน future ที่ผิดพลาด
  • แม้จะยังไม่มีวิธีแก้ที่สมบูรณ์ แต่สามารถ ลดปัญหาที่เกิดจากการยกเลิกได้ ด้วยการใช้ API ที่ปลอดภัยต่อการยกเลิก, การ pin future และการแยก task

บทนำ

  • โพสต์นี้อ้างอิงจากเนื้อหาการบรรยาย RustConf 2025 เรื่อง การจัดการการยกเลิก (cancellation) ใน Rust แบบอะซิงโครนัส
  • ในตัวอย่างโค้ดอะซิงก์ของ Rust ทั่วไป เมื่อเพิ่ม timeout เข้าไปในลูปรับหรือส่งข้อความ มักพบปัญหาข้อความสูญหายอยู่บ่อยครั้ง
  • กล่าวถึงปัญหาการยกเลิกและกรณีบั๊กจริงที่เกิดขึ้นจากการใช้งาน async Rust ในระบบขนาดใหญ่จริง เช่นที่ Oxide Computer Company
  • บทความแบ่งเป็น 3 ส่วน: 1) แนวคิดเรื่องการยกเลิก 2) การวิเคราะห์การยกเลิก 3) วิธีแก้เชิงปฏิบัติ
  • ผู้เขียนเคยประสบทั้งข้อดีและความยากของ Rust แบบอะซิงโครนัส ผ่านงานด้าน Rust signal handling และการพัฒนา cargo-nextest

1. การยกเลิกคืออะไร?

ความหมายของการยกเลิก

  • การยกเลิก (cancellation) คือสถานการณ์ที่เริ่มงานอะซิงโครนัสบางอย่างแล้วหยุดมันระหว่างทาง
  • ตัวอย่าง: การดาวน์โหลดขนาดใหญ่/คำขอเครือข่าย, การอ่านไฟล์บางส่วน ซึ่งอาจถูกยกเลิกกลางคันได้

วิธีการยกเลิกใน Rust แบบซิงโครนัส

  • โดยทั่วไปจะใช้ atomic flag เพื่อตรวจสอบเป็นระยะว่าควรยกเลิกหรือไม่ หรือใช้ข้อยกเว้นพิเศษอย่าง panic หรือบังคับจบทั้งโปรเซส
  • บางเฟรมเวิร์ก (เช่น Salsa) ใช้ panic payload แต่ไม่สามารถทำงานได้บนทุกแพลตฟอร์มของ Rust (โดยเฉพาะสภาพแวดล้อมอย่าง Wasm)
  • การบังคับหยุดเฉพาะเธรดไม่ถูกอนุญาต เนื่องจากข้อกำหนดด้านความปลอดภัยของ Rust และโครงสร้าง mutex
  • สรุปคือ ใน Rust แบบซิงโครนัส ยังไม่มีโปรโตคอลการยกเลิกที่ใช้ได้ทั่วไปและปลอดภัย

ใน Rust แบบอะซิงโครนัส: Future คืออะไร?

  • Future คือ state machine ที่คอมไพเลอร์ Rust สร้างขึ้น ซึ่งเป็นเพียงข้อมูลธรรมดาในหน่วยความจำ
  • การสร้าง future อย่างเดียวไม่ได้ทำให้มันรัน และจะคืบหน้าก็ต่อเมื่อมีการเรียก await หรือ poll เท่านั้น
  • Future ของ Rust เป็นสิ่งที่ไม่ทำงานเอง (inert) หากไม่มีการ poll/await อย่างชัดเจน มันจะไม่ประมวลผลงานใดๆ
  • ต่างจาก Go/JavaScript/C# ที่เมื่อสร้าง future แล้วมักเริ่มทำงานทันที

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

  • การ ยกเลิก Future ทำได้ง่ายๆ ด้วยการ drop มันทิ้ง หรือหยุดเรียก poll/await ต่อ
  • เพราะมันเป็น state machine จึงสามารถทิ้ง Future ได้ทุกเมื่อ
  • ใน Rust แบบอะซิงโครนัส การยกเลิกจึงทรงพลังและใช้งานได้ง่ายมาก
  • แต่ก็ ง่ายเกินไป จน future อาจถูก drop อย่างเงียบๆ และทำให้ child future ถูกยกเลิกต่อเนื่องตามไปด้วยตามโมเดล ownership
  • ด้วยคุณสมบัตินี้ การยกเลิกจึงเป็นปรากฏการณ์แบบ ไม่จำกัดอยู่เฉพาะจุด (non-local) และส่งผลต่อทั้งสายการเรียกได้

2. การวิเคราะห์การยกเลิก

ความปลอดภัยเมื่อยกเลิก และความถูกต้องของการยกเลิก

  • ความปลอดภัยเมื่อยกเลิก (cancel safety): คุณสมบัติที่ future แต่ละตัวสามารถถูกยกเลิกได้อย่างปลอดภัยโดยไม่มีผลข้างเคียง
    • ตัวอย่าง: future ของ Tokio สำหรับ sleep มี cancel safety
    • ในทางกลับกัน MPSC send ของ Tokio มีความเสี่ยงที่ข้อความจะสูญหายเมื่อถูก drop (จึงไม่มี cancel safety)
  • ความถูกต้องของการยกเลิก (cancel correctness): เป็นคุณสมบัติระดับระบบโดยรวมว่าเมื่อเกิดการยกเลิกแล้ว ระบบยังคงรักษาคุณสมบัติสำคัญไว้ได้
    • ถ้า future ที่ไม่ปลอดภัยต่อการยกเลิก ไม่มีอยู่ในระบบ ก็จะไม่มีปัญหาเรื่อง correctness
    • ปัญหาจะเกิดก็ต่อเมื่อ future ที่ไม่ cancel-safe ถูกยกเลิกจริง
    • หากการยกเลิกทำให้ข้อมูลสูญหาย, invariant ถูกละเมิด หรือ cleanup ไม่ครบ ก็ถือว่าละเมิด cancel correctness

ความยากของ Tokio mutex

  • Tokio mutex ทำงานโดยล็อกข้อมูล ปรับสถานะข้อมูล แล้วจึงปล่อยล็อก
  • ปัญหาคือ ภายในล็อกอาจมีการทำให้สถานะผิด invariant ชั่วคราว (เช่นเปลี่ยน Option<T> เป็น None) แล้วหากผ่าน await และ future ถูกยกเลิก ข้อมูลอาจค้างอยู่ในสถานะที่ผิด
  • ในงานจริง (เช่นการจัดการสถานะ sled ที่ Oxide) เคยเกิดสถานะไม่เสถียรขึ้นจากการยกเลิกที่จุด await
  • นี่ทำให้เห็นว่า การยกเลิกเป็นสาเหตุของข้อบกพร่องที่อันตรายมากในการจัดการสถานะของโค้ดอะซิงโครนัส

รูปแบบและตัวอย่างของการเกิดการยกเลิก

  • เรียก future โดยลืม .await: Rust จะเตือนสำหรับ future ที่ไม่ได้ใช้งาน แต่ถ้ารับค่าที่คืนมาเป็น _ อาจไม่มีคำเตือน (ควรใช้ lint ล่าสุดของ Clippy)
  • การดำเนินการแบบ try เช่น try_join: เมื่อ future หนึ่งล้มเหลว จะทำให้ future ที่เหลือถูกยกเลิกด้วย (และเคยกลายเป็นบั๊กในลอจิกหยุดบริการจริง)
  • แมโคร select: เมื่อรันหลาย future แบบขนาน future ที่ไม่ใช่ตัวที่เสร็จก่อนจะถูกยกเลิกทั้งหมด (เสี่ยงสูงต่อข้อมูลสูญหายในลูป select)
  • แม้รูปแบบเหล่านี้จะถูกกล่าวถึงในเอกสาร แต่ในทางปฏิบัติการยกเลิกแบบอะซิงก์สามารถเกิดขึ้นโดยปริยายได้ในหลายจุด

3. เราทำอะไรได้บ้าง?

  • ปัจจุบันยังไม่มีวิธีแก้ปัญหา cancel correctness ที่เป็นรากฐานและสมบูรณ์
  • แต่ในทางปฏิบัติ สามารถลดโอกาสเกิดข้อบกพร่องจากการยกเลิกได้ด้วยวิธีต่อไปนี้

ปรับโครงสร้างให้เป็น future ที่ปลอดภัยต่อการยกเลิก

  • ตัวอย่าง MPSC send: แยกการจอง (reserve) ออกจากการส่งจริง (send) เพื่อให้มี cancel safety บางส่วน
    • การจองสามารถถูกยกเลิกได้โดยไม่ทำให้ข้อความสูญหาย
    • เมื่อได้ permit แล้ว ก็สามารถส่งได้โดยไม่ต้องกังวลเรื่องการยกเลิก
  • สำหรับ AsyncWrite ของ write_all: การเขียนทั้งบัฟเฟอร์ด้วย write_all ไม่เสถียรต่อการยกเลิก แต่ write_all_buf ใช้ cursor ของบัฟเฟอร์เพื่อติดตามความคืบหน้าเมื่อถูกยกเลิก
    • ทำให้สามารถกลับมาทำงานต่อจากความคืบหน้าบางส่วนในลูปได้อย่างปลอดภัยด้วย write_all_buf

การใช้งาน future โดยหลีกเลี่ยงการยกเลิก

  • future pinning: ในลูป select เป็นต้น สามารถตรึง future ด้วย pin เพื่อไม่ให้ถูกยกเลิก และ poll ผ่านการอ้างอิงแทน
    • ตัวอย่าง: หากนำ future ของ reserve กลับมาใช้ซ้ำ ก็จะยังคงลำดับคิวรอการจองไว้ได้
  • การใช้ task: หากรัน future เป็น task ด้วย tokio::spawn เป็นต้น ต่อให้ drop handle ไป task เองก็ยังถูกจัดการต่อโดย runtime และจะไม่ถูกบังคับยกเลิกทันที
    • ตัวอย่างเช่นในเซิร์ฟเวอร์ HTTP ของ Oxide Dropshot จะรันแต่ละคำขอเป็น task แยก ทำให้แม้การเชื่อมต่อของไคลเอนต์จะหลุดไป ก็ยังรับประกันได้ว่าการประมวลผลคำขอจะเสร็จสิ้น

แนวทางแก้อย่างเป็นระบบ?

  • ในระดับ safe Rust ปัจจุบันยังมีข้อจำกัด แต่มีแนวทางที่กำลังถูกพูดถึงอยู่ เช่น
    • Async drop: อนุญาตให้รันโค้ด cleanup แบบอะซิงโครนัสเมื่อ future ถูกยกเลิก
    • Linear types: บังคับให้มีการรันโค้ดบางอย่างเมื่อ drop หรือระบุว่า future บางตัวห้ามยกเลิก
  • อย่างไรก็ตาม ทุกแนวทางเหล่านี้ยังมีความยากด้านการนำไปใช้งานจริง

บทสรุปและข้อแนะนำ

  • ต้องเข้าใจให้ชัดว่าคุณสมบัติพื้นฐานของ Future คือเป็นสิ่งที่ไม่ทำงานเอง (passive)
  • ควรทำความเข้าใจแนวคิด cancel safety และ cancel correctness
  • ควรรู้จักกรณีบั๊กจากการยกเลิกและรูปแบบโค้ดสำคัญ เพื่อเตรียมกลยุทธ์รับมือไว้ล่วงหน้า
  • ข้อแนะนำเชิงปฏิบัติบางส่วน
    • หลีกเลี่ยงการใช้ Tokio mutex และพิจารณาทางเลือกอื่น
    • ออกแบบหรือใช้ API แบบ partially-complete หรือ API ที่ปลอดภัยต่อการยกเลิก
    • สำหรับ future ที่ไม่ cancel-safe ควรใช้โครงสร้างโค้ดที่รับประกันว่ามันจะทำงานจนเสร็จ
  • นอกจากนี้ยังควรพิจารณาหัวข้อเชิงลึกอย่าง cooperative cancellation, actor model, structured concurrency, panic safety และ mutex poisoning
  • ดูข้อมูลเพิ่มเติมได้ที่ sunshowers/cancelling-async-rust

ขอบคุณที่อ่าน และขอขอบคุณเพื่อนร่วมงานที่ Oxide ที่ช่วยตรวจทานการบรรยายและเอกสารอ้างอิง รวมถึงให้ข้อเสนอแนะ

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

 
GN⁺ 2025-10-05
ความคิดเห็น Hacker News
  • คิดว่าตัวอย่างการใส่ timeout ให้กับ send/recv น่าสนใจมาก ทำให้เห็นว่าในภาษาที่ future เริ่มทำงานได้ทันทีโดยไม่ต้องถูก poll ตอนยังไม่รัน สถานการณ์อาจออกมาตรงกันข้ามได้ การใส่ timeout ให้ send อาจทำให้หลัง timeout แล้วยังส่งข้อความได้อยู่ แต่ข้อความจะไม่สูญหายจึงยังปลอดภัย ทว่าเมื่อใส่ timeout ให้ recv อาจเกิดกรณีที่อ่านข้อความจาก channel แล้วฝั่ง timeout ถูกเลือก ทำให้ข้อความนั้นถูกทิ้งไปเฉยๆ จึงอาจไม่ปลอดภัย วิธีแก้คือให้เลือก timeout หรือเลือกจาก channel ว่า "มีบางอย่างพร้อมใช้งาน" และในกรณีหลังให้ดูข้อมูลอย่างปลอดภัยด้วย peek
    • กำลังคิดว่านี่แหละคือหัวใจของ cancellation-safety ใช่ไหม
    • คิดว่าเป็นข้อสังเกตที่ดี
  • อยากแนะนำเอกสารบางชิ้นที่ฉันเขียนเกี่ยวกับหัวข้อนี้
    • ฉันเคยเขียนข้อเสนอไว้ในปี 2020 ว่า async function ควรถูกบังคับให้รันจนจบ โดยมีฟีเจอร์ graceful cancellation รวมอยู่ด้วย และฉันยังคิดว่าจนถึงตอนนี้ก็ยังไม่มีไอเดียที่ดีกว่านี้ ลิงก์ข้อเสนอ
    • ยังมีข้อเสนอสำหรับ unified cancellation ที่ครอบคลุมทั้ง sync และ async Rust ("A case for CancellationTokens") ลิงก์ gist
    • และยังมีตัวอย่างการนำแนวคิดข้างต้นไปใช้งานจริง min_cancel_token
  • ยังไม่ค่อยเข้าใจว่าการที่ futures ถูกยกเลิกมันเป็นปัญหาอย่างไร futures ไม่ใช่ task และตัวบทความเองก็ยอมรับจุดนี้ภายในอยู่แล้ว ถ้าอย่างนั้นการที่ future จะรันไม่จนจบก็ดูเป็นเรื่องปกติไม่ใช่หรือ และผมก็ยังไม่เข้าใจว่าทำไมสถานการณ์แบบนั้นถึงเป็นปัญหา ในตัวอย่างเขาเรียกว่า future แบบ "cancel unsafe" แต่ผมคิดว่าประเด็นหลักคือความเข้าใจไม่ตรงกันระหว่างสิ่งที่คาดหวังกับความเป็นจริง
    • ตัวอย่าง 1 คือระหว่าง try_join มีตัวหนึ่ง error เลยถูก cancel
    • ตัวอย่าง 2 คือเมื่อถูกยกเลิกแล้วข้อมูลไม่ถูกเขียน
      กรณีเหล่านี้ทั้งหมดบริบทถูก cancel จนงานไม่เสร็จ ถือเป็นพฤติกรรมที่คาดได้อยู่แล้ว ถ้างานนั้นจำเป็นต้องเสร็จจริงๆ ก็แยกไปเป็น task อิสระก็พอ ผมเลยสงสัยว่าตัวเองกำลังพลาด nuance สำคัญอะไรไปหรือเปล่า เดิมทีผมเข้าใจว่าการที่ work หายไปเพราะ cancellation นั้นเป็นเจตนาของการออกแบบ futures อยู่แล้ว อยากให้ช่วยชี้อีกทีว่าปัญหามันอยู่ตรงไหน
    • เห็นด้วยเลย! ที่ Oxide เคยเกิดบั๊กจากเรื่องนี้เยอะมาก พอเข้าใจจริงๆ ว่า futures เป็นสิ่ง passive และถูกยกเลิกได้ทุกเมื่อที่จุด await สิ่งที่เหลือก็เป็นเรื่องเทคนิคยิบย่อย
  • ตอนฟังการบรรยายนี้ใน RustConf สนุกมาก การแยกแนวคิดระหว่าง cancel safety กับ cancel correctness มีประโยชน์จริงๆ ดีใจมากที่เอาเนื้อหามาลงเป็นบล็อกโพสต์ด้วย แม้การบรรยายจะดี แต่พอเป็นบทความบล็อกแล้วแชร์และอ้างอิงได้ง่ายกว่าเยอะ
    • ฉันชอบคำว่า "cancel correctness" เพราะช่วยวางบริบทของ cancellation ได้ดี ในทางกลับกันฉันไม่ค่อยชอบคำว่า "cancel safety" เท่าไร เพราะมันไม่ค่อยตรงกับแนวคิดเรื่อง safety ของ Rust และให้ความรู้สึกตัดสินเกินจำเป็น safe/unsafe ฟังเหมือนบอกว่าอะไรดีกว่าหรือแย่กว่า ทั้งที่ความพึงประสงค์ของพฤติกรรมตอน cancel ขึ้นกับบริบท เช่น future ที่ใช้รอ task ที่ spawn ไปแล้วมักถูกเรียกว่า "cancellation safe" แต่ถ้า drop แล้ว task ยังรันต่อก็อาจสะสมงานที่ไม่จำเป็นและยังกิน lock หรือ port อยู่จนกลายเป็นปัญหาได้ ตรงกันข้าม spawn handle ที่หยุด task ได้เมื่อถูก drop แม้จะถูกเรียกว่า "cancellation unsafe" แต่กลับเป็นแพตเทิร์นที่สำคัญมากสำหรับการ cleanup ของ dependent task
    • คิดว่าบทความบล็อกอ่านง่ายและดีกว่า เห็นด้วย
  • เนื้อหาใน https://sunshowers.io/posts/cancelling-async-rust/#the-pain-of-tokio-mutexes น่าสนใจเป็นพิเศษ ฉันเองก็น่าจะพลาดแบบนั้นได้ง่ายๆ
    • ถึงฉันจะเป็นนักพัฒนา Go ส่วนนี้ก็ยังมีประโยชน์ Rust มีเครื่องมือที่เข้มงวดกว่าช่วยได้มาก แต่ใน Go ก็เผลอตกหลุมพรางแบบเดียวกันได้ง่ายจาก goroutines, channels, select และ concurrency primitives อื่นๆ
  • ในตัวอย่างแรกยังไม่ชัดว่าพฤติกรรมที่ต้องการคืออะไร ถ้าคิวเต็มก็ต้องเลือกว่า drop, รอ หรือ panic การใส่ timeout ให้การบล็อกมักทำเพื่อจับ deadlock โค้ดบอกว่า "ไม่ใช่ทุกข้อความจะเข้า channel ได้" ซึ่งก็แน่อยู่แล้วถ้าทรัพยากรไม่พอ แล้วเป้าหมายคืออะไร? การปิดโปรแกรมอย่างเรียบร้อย? นั่นค่อนข้างยากในสภาพแวดล้อมแบบ thread และใน async ก็ไม่ได้ง่ายเช่นกัน use case จริงคือการแลกเปลี่ยนข้อความกับปลายทางระยะไกลแล้วต้องเคลียร์สถานะฝั่งเราเมื่ออีกฝั่งหลุดไป
    • ถ้าเป็นไปได้ก็คงอยากเก็บข้อความไว้ในบัฟเฟอร์จนกว่าจะมีที่ว่างใน channel ซึ่งประเด็นนี้พูดถึงในช่วงท้ายของการบรรยายส่วน "What can be done"
    • ในตัวอย่างมีคำตอบอยู่แล้ว โค้ดที่เอาไว้ log เมื่อไม่มีที่ว่างนาน 5 วินาทีมีไว้เพื่อการวินิจฉัย แต่กลับแฝงความเสี่ยงทำให้ข้อมูลสูญหายได้ ถึงจะดูประดิษฐ์ไปหน่อย แต่ในโลกจริงก็เป็นโค้ดประเภท "ทำไมมันไม่ทำงาน?" ที่คนชอบแปะเพิ่มไว้ทั่วระบบได้ง่าย
    • เพื่อความถูกต้อง ผู้เขียนบทความนี้ใช้สรรพนาม they/she about
  • ควรจำไว้เสมอว่า await เป็นจุดที่อาจคืนการควบคุมได้ทุกเมื่อ การวาง await ไว้ระหว่างสองแอ็กชันที่จำเป็นต้องเกิดขึ้นแบบ atomic ควรหลีกเลี่ยง
    • อยากรู้ว่ามันทำให้เกิดปัญหาในทางปฏิบัติได้อย่างไร เช่น
      async fn a() {
        b().await
      }
      async fn b() {
        c().await
        d().await
      }
      async fn c() {}
      async fn d() {}
      
      ในโค้ดนี้ปัญหาที่ d ไม่ถูกเรียกจะเกิดขึ้นได้อย่างไร? เกิดจากมีการยกเลิกใน c หรือเกิดจากมีบางอย่างขึ้นมาจากข้างบนใน a?
    • ถ้าอย่างนั้นมันก็อันตรายพอสมควรไม่ใช่หรือ? ถึงจะหลีกเลี่ยงไม่ได้ก็ตาม แต่ถ้าใน "critical section" มี await สองครั้ง ก็อาจถูกพักไว้ตรงกลางทั้งที่ท้ายสุดจำเป็นต้องรันต่อให้จบ เช่น ถ้าเปลี่ยนข้อมูลใน DB แล้วต้องเขียน audit log ต่อ โดยทั้งสองอย่างจำเป็นต้องถูก execute แบบแน่นอน สุดท้ายมีทางออกนอกจากใส่คอมเมนต์ do not cancel หรือเปล่า
  • Future ใน Rust ก็คล้ายกับ move semantics ใน C++ ตรงที่หลังจาก Future จบแล้วมันอาจอยู่ในสถานะใช้การไม่ได้ Rust ใช้การออกแบบแบบ stackless coroutine เพราะฉะนั้นเวลาคุณ implement async แบบอิง poll เอง ก็ต้องจัดการ state ใน struct ด้วยตัวเอง จุดพวกนี้ล้วนเป็นกับดักที่เจอบ่อย และช่วงหลัง cancellation ใน async Rust ก็กลายเป็นตัวแปรใหม่ในเรื่อง state management ตอนที่ฉันพัฒนาไลบรารี mea (Make Easy Async) ถ้า cancel safety ไม่ใช่เรื่อง trivial ฉันจะเขียนเอกสารกำกับไว้เสมอ และยังจำกรณีที่ async cancellation แบบสะเพร่าทำให้เกิดปัญหาใน IO stack ได้ด้วย mea กรณีใน reddit
  • เป็นการบรรยายที่ดีมาก! ในฐานะมือใหม่สุดๆ ฉันอยากให้ใน SOP เน้นแต่แรกว่าคุณไม่สามารถ cancel Future ได้ เพราะ .await เป็นเจ้าของ future อยู่เลย drop() ไม่ได้ และเพราะ future เป็นแบบ lazy ทำให้หลัง .await ไปแล้วก็ไม่ชัดว่าการ cancel ทำงานอย่างไร หลังจากนั้นฉันไปศึกษา select! กับ Abortable() เพิ่มจนเข้าใจ แต่ถ้าครั้งหน้าจะบรรยายอีก ถ้าชี้จุดนี้ตั้งแต่ต้นก็น่าจะสมบูรณ์แบบมาก
    • คำถามนะ SOP ในที่นี้หมายถึงอะไร
  • จังหวะดีมาก วันนี้ฉันเพิ่งกำลังใส่ doc comment ให้ฟังก์ชันใหม่ว่า "ฟังก์ชันนี้ cancel safe" แล้วก็คิดเรื่องพวกนี้อยู่เลย หวังว่า async drop จะใช้ได้สักทีเร็วๆ นี้
    • อยากรู้ว่าฟังก์ชันนั้นคืออะไร เล่าเพิ่มได้ไหมด้วยความอยากรู้