หลักเกณฑ์ API แบบไม่บล็อกและแบบ Async ของ Android

API ที่ไม่บล็อกจะขอให้ดำเนินการและส่งคืนการควบคุมไปยัง เธรดที่เรียกใช้เพื่อให้เธรดดังกล่าวสามารถทำงานอื่นๆ ก่อนที่การดำเนินการที่ขอจะเสร็จสมบูรณ์ API เหล่านี้มีประโยชน์ในกรณีที่งานที่ขออาจอยู่ระหว่างดำเนินการ หรืออาจต้องรอให้ I/O หรือ IPC เสร็จสมบูรณ์ ความพร้อมใช้งานของทรัพยากรระบบที่มีการแข่งขันสูง หรือข้อมูลจากผู้ใช้ก่อนที่จะดำเนินการต่อได้ โดยเฉพาะอย่างยิ่ง API ที่ออกแบบมาอย่างดีจะช่วยให้ยกเลิกการดำเนินการ ที่กำลังดำเนินการอยู่และหยุดการทำงานในนามของผู้เรียกเดิม เพื่อรักษาสุขภาพของระบบและอายุการใช้งานแบตเตอรี่เมื่อไม่จำเป็นต้องดำเนินการอีกต่อไป

API แบบไม่พร้อมกันเป็นวิธีหนึ่งในการทำให้เกิดลักษณะการทำงานแบบไม่บล็อก API แบบไม่พร้อมกัน ยอมรับรูปแบบการดำเนินการต่อหรือการเรียกกลับบางอย่างที่จะได้รับการแจ้งเตือนเมื่อการดำเนินการ เสร็จสมบูรณ์ หรือเมื่อมีเหตุการณ์อื่นๆ ในระหว่างความคืบหน้าของการดำเนินการ

การเขียน API แบบไม่พร้อมกันมีแรงจูงใจหลักๆ 2 ประการ ดังนี้

  • การดำเนินการหลายอย่างพร้อมกัน โดยการดำเนินการที่ N จะต้องเริ่มต้นก่อนที่การดำเนินการที่ N-1 จะเสร็จสมบูรณ์
  • หลีกเลี่ยงการบล็อกเธรดการโทรจนกว่าการดำเนินการจะเสร็จสมบูรณ์

Kotlin สนับสนุนการทำงานพร้อมกันที่มีโครงสร้างอย่างยิ่ง ซึ่งเป็นชุดหลักการและ API ที่สร้างขึ้นจาก Suspend Function ซึ่งแยกการดำเนินการโค้ดแบบซิงโครนัสและแบบอะซิงโครนัสออกจากลักษณะการทำงานที่บล็อกเธรด ฟังก์ชันระงับเป็นแบบไม่บล็อกและซิงโครนัส

ระงับฟังก์ชัน

  • อย่าบล็อกเธรดการเรียกใช้ของฟังก์ชันดังกล่าว แต่ให้ส่งต่อเธรดการดำเนินการเป็น รายละเอียดการใช้งานขณะรอผลลัพธ์ของการดำเนินการที่ดำเนินการ ที่อื่นแทน
  • ดำเนินการแบบพร้อมกันและไม่กำหนดให้ผู้เรียกใช้ API ที่ไม่บล็อกดำเนินการต่อไปพร้อมกันกับงานที่ไม่บล็อกซึ่งเริ่มต้นโดยการเรียก API

หน้านี้จะอธิบายรายละเอียดพื้นฐานขั้นต่ำของความคาดหวังที่นักพัฒนาแอปสามารถมีได้อย่างปลอดภัย เมื่อทำงานกับ API แบบไม่บล็อกและแบบอะซิงโครนัส ตามด้วยชุด สูตรสำหรับการเขียน API ที่ตรงกับความคาดหวังเหล่านี้ในภาษา Kotlin หรือ Java ในแพลตฟอร์ม Android หรือไลบรารี Jetpack หากไม่แน่ใจ ให้พิจารณา ความคาดหวังของนักพัฒนาแอปเป็นข้อกำหนดสำหรับ API Surface ใหม่

สิ่งที่นักพัฒนาแอปคาดหวังจาก Async API

ความคาดหวังต่อไปนี้เขียนขึ้นจากมุมมองของ API ที่ไม่ถูกระงับ เว้นแต่จะระบุไว้เป็นอย่างอื่น

API ที่ยอมรับการเรียกกลับมักจะเป็นแบบอะซิงโครนัส

หาก API ยอมรับการเรียกกลับที่ไม่ได้ระบุไว้ว่าจะเรียกใช้ในที่เท่านั้น (กล่าวคือ เรียกใช้โดยเธรดที่เรียกใช้เท่านั้นก่อนที่การเรียก API เองจะกลับมา) ระบบจะถือว่า API เป็นแบบอะซิงโครนัส และ API นั้นควรเป็นไปตามความคาดหวังอื่นๆ ทั้งหมดที่ระบุไว้ในส่วนต่อไปนี้

ตัวอย่างของ Callback ที่เรียกใช้ในที่เดียวเสมอคือฟังก์ชัน Map หรือ Filter ระดับสูงกว่า ซึ่งเรียกใช้ Mapper หรือ Predicate ในแต่ละรายการในคอลเล็กชันก่อนที่จะแสดงผล

API แบบไม่พร้อมกันควรแสดงผลโดยเร็วที่สุด

นักพัฒนาแอปคาดหวังว่า API แบบไม่พร้อมกันจะไม่บล็อกและจะตอบกลับอย่างรวดเร็วหลังจาก เริ่มคำขอสำหรับการดำเนินการ คุณควรเรียกใช้ API แบบไม่พร้อมกันได้อย่างปลอดภัยทุกเมื่อ และการเรียกใช้ API แบบไม่พร้อมกันไม่ควรทำให้เกิดเฟรมที่กระตุกหรือ ANR

แพลตฟอร์มหรือไลบรารีสามารถทริกเกอร์สัญญาณการดำเนินการและวงจรชีวิตได้ตามต้องการ และการคาดหวังให้นักพัฒนาแอปมีความรู้ในระดับโลกเกี่ยวกับเว็บไซต์ที่อาจเรียกใช้โค้ดของตนทั้งหมดนั้นเป็นไปไม่ได้ เช่น คุณอาจเพิ่ม Fragment ลงใน FragmentManager ในธุรกรรมแบบซิงโครนัสในการตอบกลับ การวัดและเลย์เอาต์ของ View เมื่อต้องป้อนเนื้อหาแอปเพื่อเติม พื้นที่ว่าง (เช่น RecyclerView) LifecycleObserver ที่ตอบกลับ Lifecycle Callback onStart ของ Fragment นี้อาจดำเนินการเริ่มต้นแบบครั้งเดียวที่นี่ได้อย่างสมเหตุสมผล และอาจอยู่ในเส้นทางโค้ดที่สำคัญสำหรับการสร้าง เฟรมภาพเคลื่อนไหวที่ไม่มีอาการกระตุก นักพัฒนาแอปควรมั่นใจเสมอว่า การเรียกใช้ API แบบไม่พร้อมกันใดๆ เพื่อตอบสนองต่อการเรียกกลับของวงจรนี้ จะไม่ทำให้เกิดเฟรมที่กระตุก

ซึ่งหมายความว่างานที่ API แบบไม่พร้อมกันดำเนินการก่อนที่จะส่งคืนจะต้องมีน้ำหนักเบามาก โดยอาจเป็นการสร้างบันทึกของคำขอและ Callback ที่เกี่ยวข้อง และลงทะเบียนกับเครื่องมือการดำเนินการที่ทำงานดังกล่าว หากการลงทะเบียนสำหรับการดำเนินการแบบไม่พร้อมกันต้องใช้ IPC การใช้งาน API ควร ใช้มาตรการที่จำเป็นเพื่อตอบสนองความคาดหวังของนักพัฒนาแอปนี้ โดยอาจรวมถึงสิ่งต่อไปนี้อย่างน้อย 1 อย่าง

  • การใช้ IPC พื้นฐานเป็นการเรียก Binder ทางเดียว
  • การเรียก Binder แบบ 2 ทางไปยังเซิร์ฟเวอร์ของระบบซึ่งการลงทะเบียนให้เสร็จสมบูรณ์ไม่จำเป็นต้องใช้การล็อกที่มีการแข่งขันสูง
  • โพสต์คำขอไปยังเทรดผู้ปฏิบัติงานในกระบวนการของแอปเพื่อทำการลงทะเบียนที่บล็อกผ่าน IPC

API แบบไม่พร้อมกันควรแสดงผลเป็น void และจะทิ้งเฉพาะอาร์กิวเมนต์ที่ไม่ถูกต้อง

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

API แบบไม่พร้อมกันอาจตรวจสอบอาร์กิวเมนต์สำหรับค่า Null และส่ง NullPointerException หรือ ตรวจสอบว่าอาร์กิวเมนต์ที่ระบุอยู่ในช่วงที่ถูกต้องและส่ง IllegalArgumentException ตัวอย่างเช่น สำหรับฟังก์ชันที่ยอมรับ float ในขอบเขตของ 0 ถึง 1f ฟังก์ชันอาจตรวจสอบว่าพารามิเตอร์อยู่ภายใน ขอบเขตนี้และแสดง IllegalArgumentException หากอยู่นอกขอบเขต หรืออาจตรวจสอบ String สั้นๆ เพื่อดูว่าสอดคล้องกับรูปแบบที่ถูกต้องหรือไม่ เช่น ตัวอักษรและตัวเลขเท่านั้น (โปรดทราบว่าเซิร์ฟเวอร์ระบบไม่ควรเชื่อถือกระบวนการของแอป บริการของระบบควรทำซ้ำการตรวจสอบเหล่านี้ในบริการของระบบ ด้วยตนเอง)

ควรรายงานข้อผิดพลาดอื่นๆ ทั้งหมดไปยังการเรียกกลับที่ระบุ ซึ่งรวมถึงแต่ไม่จำกัดเพียง

  • การดำเนินการที่ขอไม่สำเร็จ
  • ข้อยกเว้นด้านความปลอดภัยสำหรับการให้สิทธิ์หรือสิทธิ์ที่จำเป็นในการ ดำเนินการให้เสร็จสมบูรณ์
  • เกินโควต้าสำหรับการดำเนินการ
  • กระบวนการของแอปไม่ได้ "อยู่เบื้องหน้า" มากพอที่จะดำเนินการ
  • ฮาร์ดแวร์ที่จำเป็นถูกตัดการเชื่อมต่อ
  • เครือข่ายล้มเหลว
  • หมดเวลา
  • Binder สิ้นสุดการทำงานหรือกระบวนการระยะไกลไม่พร้อมใช้งาน

API แบบไม่พร้อมกันควรมีกลไกการยกเลิก

API แบบไม่พร้อมกันควรมีวิธีระบุให้การดำเนินการที่กำลังทำงานทราบว่า ผู้เรียกไม่สนใจผลลัพธ์อีกต่อไป การดำเนินการยกเลิกนี้ควรส่งสัญญาณ 2 อย่างต่อไปนี้

ควรปล่อยการอ้างอิงที่ชัดเจนถึง Callback ที่ผู้เรียกใช้ระบุ

การเรียกกลับที่ระบุไว้ใน API แบบไม่พร้อมกันอาจมีการอ้างอิงแบบฮาร์ดโค้ดไปยังกราฟออบเจ็กต์ขนาดใหญ่ และงานที่กำลังดำเนินการซึ่งมีการอ้างอิงแบบฮาร์ดโค้ดไปยังการเรียกกลับนั้นอาจทำให้กราฟออบเจ็กต์เหล่านั้นไม่ถูกเก็บขยะ การเผยแพร่การอ้างอิงการเรียกกลับเหล่านี้เมื่อมีการยกเลิกจะทำให้กราฟออบเจ็กต์เหล่านี้อาจมีสิทธิ์สำหรับการเก็บขยะเร็วกว่าในกรณีที่อนุญาตให้งานทำงานจนเสร็จ

เครื่องมือการดำเนินการที่ทำงานให้กับผู้โทรอาจหยุดการทำงานนั้น

งานที่เริ่มต้นโดยการเรียก API แบบไม่พร้อมกันอาจมีต้นทุนสูงในด้านการใช้พลังงานหรือ ทรัพยากรอื่นๆ ของระบบ API ที่อนุญาตให้ผู้โทรส่งสัญญาณเมื่อไม่จำเป็นต้องดำเนินการนี้อีกต่อไปจะอนุญาตให้หยุดการดำเนินการนั้นก่อนที่จะใช้ทรัพยากรระบบเพิ่มเติม

ข้อควรพิจารณาพิเศษสำหรับแอปที่แคชไว้หรือแอปที่ถูกระงับ

เมื่อออกแบบ API แบบไม่พร้อมกันซึ่งมีการเรียกกลับที่มาจากกระบวนการของระบบ และส่งไปยังแอป ให้พิจารณาสิ่งต่อไปนี้

  1. กระบวนการและวงจรชีวิตของแอป: กระบวนการของแอปผู้รับอาจอยู่ในสถานะแคช
  2. การหยุดแอปที่แคชไว้ชั่วคราว: กระบวนการของแอปผู้รับอาจถูกหยุดชั่วคราว

เมื่อกระบวนการของแอปเข้าสู่สถานะแคช หมายความว่ากระบวนการนั้นไม่ได้โฮสต์คอมโพเนนต์ที่ผู้ใช้มองเห็น เช่น กิจกรรมและบริการ ระบบจะเก็บแอปไว้ในหน่วยความจำในกรณีที่แอปกลับมาปรากฏต่อผู้ใช้อีกครั้ง แต่ในระหว่างนี้แอปไม่ควรทำงาน ในกรณีส่วนใหญ่ คุณควรหยุดการเรียกกลับของแอปชั่วคราวเมื่อแอปเข้าสู่สถานะแคช และกลับมาทำงานต่อเมื่อแอปออกจากสถานะแคช เพื่อไม่ให้เกิดการทำงานในกระบวนการของแอปที่แคชไว้

แอปที่แคชไว้อาจถูกหยุดชั่วคราวด้วย เมื่อแอปถูกระงับ แอปจะได้รับเวลา CPU เป็น 0 และไม่สามารถทำงานใดๆ ได้เลย การเรียกไปยังแฮนเดิลการเรียกกลับที่ลงทะเบียนของแอปนั้นจะได้รับการบัฟเฟอร์และส่งเมื่อแอปเลิกหยุดทำงาน

ธุรกรรมที่บัฟเฟอร์ไปยังการเรียกกลับของแอปอาจล้าสมัยเมื่อแอป เลิกตรึงและประมวลผลธุรกรรมเหล่านั้น บัฟเฟอร์มีขีดจำกัด และหากมีข้อมูลมากเกินไปจะทำให้แอปของผู้รับขัดข้อง อย่าเรียกใช้แอป Callback ขณะที่กระบวนการของแอปหยุดทำงานเพื่อไม่ให้แอปทำงานหนักเกินไปกับเหตุการณ์ที่ล้าสมัยหรือบัฟเฟอร์ล้น

อยู่ระหว่างการตรวจสอบ

  • คุณควรพิจารณาหยุดการเรียกกลับของแอปการจัดส่งชั่วคราวขณะที่ระบบแคชกระบวนการของแอป
  • คุณต้องหยุดการเรียกกลับของแอปการนำส่งชั่วคราวขณะที่กระบวนการของแอป หยุดทำงาน

การติดตามสถานะ

วิธีติดตามเมื่อแอปเข้าหรือออกจากสถานะแคช

mActivityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

วิธีติดตามเมื่อมีการหยุดหรือเลิกหยุดแอปชั่วคราว

IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);

กลยุทธ์ในการกลับมาเรียกใช้แฮนเดิลการเรียกกลับของแอป

ไม่ว่าคุณจะหยุดการเรียกกลับของแอปชั่วคราวเมื่อแอปเข้าสู่สถานะแคช หรือสถานะหยุดทำงาน เมื่อแอปออกจากสถานะดังกล่าว คุณควรกลับมา ส่งการเรียกกลับที่ลงทะเบียนของแอปอีกครั้งเมื่อแอปออกจากสถานะดังกล่าว จนกว่าแอปจะยกเลิกการลงทะเบียนการเรียกกลับหรือกระบวนการของแอปจะสิ้นสุดลง

เช่น

IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
    if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
        shouldSendCallbacks = false;
    } else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
        shouldSendCallbacks = true;
    }
});

หรือจะใช้ RemoteCallbackList ซึ่งจะดูแลไม่ให้ ส่งการเรียกกลับไปยังกระบวนการเป้าหมายเมื่อหยุดชั่วคราวก็ได้

เช่น

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));

callback.foo() จะเรียกใช้ก็ต่อเมื่อกระบวนการไม่ได้หยุดทำงาน

แอปมักจะบันทึกการอัปเดตที่ได้รับโดยใช้การเรียกกลับเป็นสแนปชอตของสถานะล่าสุด ลองพิจารณา API สมมติสำหรับแอปเพื่อตรวจสอบเปอร์เซ็นต์แบตเตอรี่ที่เหลือ

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

พิจารณาสถานการณ์ที่เหตุการณ์การเปลี่ยนสถานะหลายรายการเกิดขึ้นเมื่อแอปถูก หยุดชั่วคราว เมื่อเลิกตรึงแอป คุณควรส่งเฉพาะสถานะล่าสุดไปยังแอปและทิ้งการเปลี่ยนแปลงสถานะอื่นๆ ที่ล้าสมัย การนำส่งนี้ควรเกิดขึ้น ทันทีเมื่อเลิกตรึงแอปเพื่อให้แอป "ตามทัน" ซึ่งทำได้โดยทำดังนี้

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));

ในบางกรณี คุณอาจติดตามค่าล่าสุดที่ส่งไปยังแอปเพื่อให้แอป ไม่จําเป็นต้องได้รับการแจ้งเตือนค่าเดียวกันเมื่อเลิกตรึงแล้ว

สถานะอาจแสดงเป็นข้อมูลที่ซับซ้อนมากขึ้น ลองพิจารณา API สมมติสำหรับ แอปที่จะได้รับการแจ้งเตือนเกี่ยวกับอินเทอร์เฟซเครือข่าย

interface NetworkListener {
    void onAvailable(Network network);
    void onLost(Network network);
    void onChanged(Network network);
}

เมื่อหยุดการแจ้งเตือนชั่วคราวสำหรับแอป คุณควรจดจำชุดเครือข่าย และสถานะที่แอปเห็นล่าสุด เมื่อกลับมาทำงานอีกครั้ง เราขอแนะนำให้แจ้ง แอปเกี่ยวกับเครือข่ายเก่าที่ขาดหายไป เครือข่ายใหม่ที่พร้อมใช้งาน และเครือข่ายที่มีอยู่ซึ่งมีการเปลี่ยนแปลงสถานะ โดยเรียงตามลำดับนี้

อย่าแจ้งเตือนแอปเกี่ยวกับเครือข่ายที่พร้อมใช้งานแล้วหายไปขณะที่หยุดการเรียกกลับชั่วคราว แอปไม่ควรได้รับบันทึกเหตุการณ์ทั้งหมดที่เกิดขึ้นขณะที่แอปถูกระงับ และเอกสารประกอบ API ไม่ควรรับประกันว่าจะส่งสตรีมเหตุการณ์อย่างต่อเนื่องนอกสถานะวงจรที่ชัดเจน ใน ตัวอย่างนี้ หากแอปต้องตรวจสอบความพร้อมใช้งานของเครือข่ายอย่างต่อเนื่อง แอปจะต้องอยู่ในสถานะวงจรของแอปที่ป้องกันไม่ให้แอปถูกแคชหรือ หยุดทำงาน

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

ข้อควรพิจารณาสำหรับเอกสารประกอบสำหรับนักพัฒนาแอป

การนำส่งเหตุการณ์แบบไม่พร้อมกันอาจล่าช้า เนื่องจากผู้ส่งหยุดชั่วคราว การนำส่งเป็นระยะเวลาหนึ่งตามที่แสดงในส่วนก่อนหน้า หรือเนื่องจาก แอปผู้รับไม่ได้รับทรัพยากรของอุปกรณ์เพียงพอที่จะประมวลผลเหตุการณ์ได้ อย่างทันท่วงที

ไม่สนับสนุนให้นักพัฒนาแอปคาดเดาเวลาที่แอปได้รับการแจ้งเตือนเกี่ยวกับเหตุการณ์กับเวลาที่เกิดเหตุการณ์จริง

สิ่งที่นักพัฒนาแอปคาดหวังเมื่อมีการระงับ API

นักพัฒนาแอปที่คุ้นเคยกับการทำงานพร้อมกันแบบมีโครงสร้างของ Kotlin คาดหวังพฤติกรรมต่อไปนี้จาก API ที่ระงับการทำงาน

ฟังก์ชันระงับควรทำงานที่เกี่ยวข้องทั้งหมดให้เสร็จก่อนที่จะส่งคืนหรือส่งข้อยกเว้น

ผลลัพธ์ของการดำเนินการที่ไม่บล็อกจะแสดงผลเป็นค่าการแสดงผลของฟังก์ชันปกติ และข้อผิดพลาดจะรายงานโดยการส่งข้อยกเว้น (ซึ่งมักหมายความว่าไม่จำเป็นต้องใช้พารามิเตอร์ การเรียกกลับ)

ฟังก์ชันระงับควรเรียกใช้พารามิเตอร์ Callback ในที่เดียวเท่านั้น

ฟังก์ชันระงับควรทำงานที่เกี่ยวข้องทั้งหมดให้เสร็จสมบูรณ์ก่อนที่จะส่งคืนเสมอ ดังนั้นจึงไม่ควรเรียกใช้ Callback ที่ระบุหรือพารามิเตอร์ฟังก์ชันอื่นๆ หรือเก็บการอ้างอิงถึงพารามิเตอร์ดังกล่าวหลังจากที่ฟังก์ชันระงับส่งคืนแล้ว

ฟังก์ชันระงับที่ยอมรับพารามิเตอร์การเรียกกลับควรเก็บรักษาบริบทไว้ เว้นแต่จะระบุไว้เป็นอย่างอื่น

การเรียกใช้ฟังก์ชันในฟังก์ชันระงับจะทำให้ฟังก์ชันนั้นทำงานใน CoroutineContext ของผู้เรียก เนื่องจากฟังก์ชันระงับควรทำงานที่เกี่ยวข้องทั้งหมดให้เสร็จสิ้นก่อนที่จะส่งคืนหรือส่งข้อยกเว้น และควรเรียกใช้พารามิเตอร์การเรียกกลับในที่เดียวเท่านั้น ค่าเริ่มต้นที่คาดไว้คือการเรียกกลับดังกล่าวจะทำงานใน CoroutineContext ที่เรียกใช้โดยใช้ Dispatcher ที่เกี่ยวข้อง หากวัตถุประสงค์ของ API คือการเรียกใช้ Callback นอกการเรียก CoroutineContext คุณควรบันทึกลักษณะการทำงานนี้อย่างชัดเจน

ฟังก์ชันระงับควรรองรับการยกเลิกงาน kotlinx.coroutines

ฟังก์ชันระงับใดๆ ที่เสนอควรทำงานร่วมกับการยกเลิกงานตามที่ kotlinx.coroutines กำหนด หากมีการยกเลิกงานการเรียกของการดำเนินการที่กำลังดำเนินการอยู่ ฟังก์ชันควรกลับมาทำงานต่อด้วย CancellationException โดยเร็วที่สุดเพื่อให้ผู้เรียกสามารถล้างข้อมูลและดำเนินการต่อได้โดยเร็วที่สุด suspendCancellableCoroutine และ API การระงับอื่นๆ ที่ kotlinx.coroutines นำเสนอจะจัดการเรื่องนี้โดยอัตโนมัติ โดยทั่วไปแล้ว การติดตั้งใช้งานไลบรารีไม่ควรใช้ suspendCoroutine โดยตรง เนื่องจากไม่รองรับลักษณะการยกเลิกนี้โดยค่าเริ่มต้น

ฟังก์ชัน Suspend ที่ทำงานแบบบล็อกในเบื้องหลัง (ไม่ใช่เธรดหลักหรือเธรด UI) ต้องมีวิธีในการกำหนดค่า Dispatcher ที่ใช้

ไม่แนะนำให้ใช้ฟังก์ชันบล็อกเพื่อระงับเธรดทั้งหมดเพื่อ สลับเธรด

การเรียกฟังก์ชันระงับไม่ควรส่งผลให้มีการสร้างเธรดเพิ่มเติม โดยไม่ได้รับอนุญาตให้นักพัฒนาแอปจัดหาเธรดหรือพูลเธรดของตนเอง เพื่อทำงานดังกล่าว ตัวอย่างเช่น เครื่องมือสร้างอาจยอมรับCoroutineContextที่ใช้เพื่อทำงานเบื้องหลังสำหรับเมธอดของคลาส

ฟังก์ชันที่ระงับซึ่งจะยอมรับพารามิเตอร์ CoroutineContext หรือ Dispatcher ที่ไม่บังคับเฉพาะเพื่อเปลี่ยนไปใช้ Dispatcher นั้นเพื่อทำการบล็อก ควรแสดงฟังก์ชันการบล็อกพื้นฐานแทน และแนะนำให้ นักพัฒนาแอปที่เรียกใช้ใช้การเรียกของตนเองกับ withContext เพื่อส่งงานไปยัง Dispatcher ที่เลือก

คลาสที่เปิดตัวโครูทีน

คลาสที่เปิดใช้โครูทีนต้องมี CoroutineScope เพื่อดำเนินการเปิดใช้เหล่านั้น การปฏิบัติตามหลักการของความพร้อมกันที่มีโครงสร้างหมายถึงรูปแบบโครงสร้างต่อไปนี้สำหรับการรับและจัดการขอบเขตดังกล่าว

ก่อนที่จะเขียนคลาสที่เปิดใช้เทสพร้อมกันในขอบเขตอื่น ให้พิจารณารูปแบบอื่นแทน

class MyClass {
    private val requests = Channel<MyRequest>(Channel.UNLIMITED)

    suspend fun handleRequests() {
        coroutineScope {
            for (request in requests) {
                // Allow requests to be processed concurrently;
                // alternatively, omit the [launch] and outer [coroutineScope]
                // to process requests serially
                launch {
                    processRequest(request)
                }
            }
        }
    }

    fun submitRequest(request: MyRequest) {
        requests.trySend(request).getOrThrow()
    }
}

การเปิดเผย suspend fun เพื่อทำงานพร้อมกันช่วยให้ผู้เรียกใช้สามารถเรียกใช้ การดำเนินการในบริบทของตนเองได้ ซึ่งทำให้ไม่จำเป็นต้องให้ MyClass จัดการ CoroutineScope การประมวลผลคำขอแบบอนุกรมจะง่ายขึ้น และมักจะใช้ตัวแปรภายในของ handleRequests แทนพร็อพเพอร์ตี้ของคลาส ซึ่งอาจต้องมีการซิงค์เพิ่มเติม

คลาสที่จัดการโครูทีนควรแสดงเมธอดปิดและยกเลิก

คลาสที่เปิดใช้โครูทีนเป็นรายละเอียดการใช้งานต้องมีวิธีปิดงานพร้อมกันที่กำลังดำเนินการอยู่เหล่านั้นอย่างสะอาด เพื่อไม่ให้มีการรั่วไหลของงานพร้อมกันที่ไม่ได้ควบคุมไปยังขอบเขตระดับบน โดยปกติแล้ว การดำเนินการนี้จะอยู่ในรูปแบบ ของการสร้างJobย่อยของCoroutineContextที่ระบุ

private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)

fun cancel() {
    myJob.cancel()
}

นอกจากนี้ยังอาจมีjoin()เมธอดที่อนุญาตให้โค้ดของผู้ใช้รอ การทำงานพร้อมกันที่ยังค้างอยู่ซึ่งออบเจ็กต์กำลังดำเนินการให้เสร็จสมบูรณ์ (ซึ่งอาจรวมถึงการล้างข้อมูลที่ดำเนินการโดยการยกเลิกการดำเนินการ)

suspend fun join() {
    myJob.join()
}

การตั้งชื่อการดำเนินการของเทอร์มินัล

ชื่อที่ใช้สำหรับวิธีการที่ปิดงานพร้อมกันที่ออบเจ็กต์เป็นเจ้าของอย่างเรียบร้อย ซึ่งยังคงดำเนินการอยู่ ควรแสดงถึงสัญญาด้านพฤติกรรมของวิธีการปิดระบบ

ใช้ close() เมื่อการดำเนินการที่กำลังดำเนินการอาจเสร็จสมบูรณ์ แต่จะเริ่มการดำเนินการใหม่ไม่ได้ หลังจากที่การเรียกใช้ close() กลับมา

ใช้ cancel() เมื่ออาจมีการยกเลิกการดำเนินการที่กำลังดำเนินการอยู่ก่อนที่จะเสร็จสมบูรณ์ ห้ามเริ่มการดำเนินการใหม่หลังจากที่การเรียกใช้ cancel() กลับมา

ตัวสร้างคลาสยอมรับ CoroutineContext ไม่ใช่ CoroutineScope

เมื่อไม่อนุญาตให้ออบเจ็กต์เปิดตัวโดยตรงในขอบเขตระดับบนสุดที่ระบุ ความเหมาะสมของ CoroutineScope เป็นพารามิเตอร์ของตัวสร้างจะลดลง

// Don't do this
class MyClass(scope: CoroutineScope) {
    private val myJob = Job(parent = scope.`CoroutineContext`[Job])
    private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)

    // ... the [scope] constructor parameter is never used again
}

CoroutineScope กลายเป็น Wrapper ที่ไม่จำเป็นและทำให้เข้าใจผิด ซึ่งในบางกรณีการใช้งานอาจสร้างขึ้นเพื่อส่งเป็นพารามิเตอร์ของตัวสร้างเท่านั้น แล้วจึงทิ้งไป

// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))

พารามิเตอร์ CoroutineContext จะมีค่าเริ่มต้นเป็น EmptyCoroutineContext

เมื่อพารามิเตอร์ CoroutineContext ที่ไม่บังคับปรากฏใน API Surface ค่าเริ่มต้นต้องเป็น Empty`CoroutineContext` Sentinel ซึ่งจะช่วยให้ การทำงานของ API ดีขึ้น เนื่องจากระบบจะถือว่าEmpty`CoroutineContext`ค่า จากผู้เรียกใช้เหมือนกับการยอมรับค่าเริ่มต้น

class MyOuterClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val innerObject = MyInnerClass(`CoroutineContext`)

    // ...
}

class MyInnerClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val job = Job(parent = `CoroutineContext`[Job])
    private val scope = CoroutineScope(`CoroutineContext` + job)

    // ...
}