โครูทีน Kotlin ช่วยให้คุณเขียนโค้ดแบบอะซิงโครนัสที่สะอาดและเรียบง่ายซึ่งจะช่วยให้ แอปตอบสนองได้ดีขณะจัดการงานที่ใช้เวลานาน เช่น การเรียกเครือข่าย หรือการดำเนินการในดิสก์
หัวข้อนี้จะเจาะลึกเกี่ยวกับโครูทีนใน Android หากคุณไม่คุ้นเคยกับ Coroutines โปรดอ่านKotlin Coroutines ใน Android ก่อนอ่านหัวข้อนี้
จัดการงานที่ใช้เวลานาน
โครูทีนสร้างขึ้นจากฟังก์ชันปกติโดยเพิ่มการดำเนินการ 2 อย่างเพื่อจัดการ
งานที่ใช้เวลานาน นอกจาก invoke (หรือ call) และ return แล้ว
โครูทีนยังเพิ่ม suspend และ resume ด้วย
suspendจะหยุดการดำเนินการของโครูทีนปัจจุบันชั่วคราวและบันทึกตัวแปรภายในทั้งหมดresumeจะดำเนินการต่อจากโครูทีนที่ถูกระงับจากตำแหน่ง ที่ถูกระงับ
คุณเรียกใช้ฟังก์ชัน suspend ได้จากฟังก์ชัน suspend อื่นๆ เท่านั้น หรือใช้ตัวสร้างโครูทีน เช่น launch เพื่อเริ่มโครูทีนใหม่
ตัวอย่างต่อไปนี้แสดงการใช้งานโครูทีนอย่างง่ายสำหรับ งานที่ใช้เวลานานสมมติ
suspend fun fetchDocs() { // Dispatchers.Main val result = get("https://developer.android.com") // Dispatchers.IO for `get` show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
ในตัวอย่างนี้ get() ยังคงทำงานในเทรดหลัก แต่จะระงับ
โครูทีนก่อนที่จะเริ่มคำขอเครือข่าย เมื่อคำขอเครือข่าย
เสร็จสมบูรณ์ get จะกลับมาทำงานต่อในโครูทีนที่ระงับไว้แทนที่จะใช้โค้ดเรียกกลับ
เพื่อแจ้งเตือนเทรดหลัก
Kotlin ใช้สแตกเฟรมเพื่อจัดการฟังก์ชันที่กำลังทำงานพร้อมกับตัวแปรภายใน เมื่อหยุดโครูทีนชั่วคราว ระบบจะคัดลอกและบันทึกเฟรมสแต็กปัจจุบัน ไว้ใช้ในภายหลัง เมื่อกลับมาทำงานต่อ ระบบจะคัดลอกสแตกเฟรมกลับจากตำแหน่งที่บันทึกไว้ และฟังก์ชันจะเริ่มทำงานอีกครั้ง แม้ว่าโค้ดอาจดูเหมือนคำขอการบล็อกตามลำดับปกติ แต่ Coroutine จะช่วยให้คำขอเครือข่ายหลีกเลี่ยงการบล็อก เทรดหลัก
ใช้โครูทีนเพื่อความปลอดภัยของ Main
โคโรทีน Kotlin ใช้ตัวจัดสรรเพื่อกำหนดว่าควรใช้เธรดใดในการ ดำเนินการโคโรทีน หากต้องการเรียกใช้โค้ดนอกเทรดหลัก คุณสามารถบอกให้โคโรทีนของ Kotlin ดำเนินการใน Dispatcher Default หรือ IO ก็ได้ ใน Kotlin โครูทีนทั้งหมดต้องทำงานใน Dispatcher แม้ว่าจะทำงานในเทรดหลักก็ตาม โครูทีนสามารถระงับตัวเองได้ และ Dispatcher มีหน้าที่รับผิดชอบในการกลับมาทำงานต่อ
Kotlin มี Dispatcher 3 รายการที่คุณใช้ได้เพื่อระบุตำแหน่งที่ควรเรียกใช้ Coroutine
- Dispatchers.Main - ใช้ Dispatcher นี้เพื่อเรียกใช้ Coroutine ในเทรดหลักของ Android ควรใช้เพื่อโต้ตอบกับ UI และ
ทำงานอย่างรวดเร็วเท่านั้น ตัวอย่างเช่น การเรียกใช้
suspendฟังก์ชัน การเรียกใช้การดำเนินการเฟรมเวิร์ก UI ของ Android และการอัปเดตออบเจ็กต์LiveData - Dispatchers.IO - Dispatcher นี้ได้รับการเพิ่มประสิทธิภาพเพื่อดำเนินการ I/O ของดิสก์หรือเครือข่าย นอกเทรดหลัก ตัวอย่างเช่น การใช้ คอมโพเนนต์ Room การอ่านหรือเขียนไฟล์ และการเรียกใช้การดำเนินการเครือข่าย
- Dispatchers.Default - Dispatcher นี้ได้รับการเพิ่มประสิทธิภาพเพื่อทำงานที่ใช้ CPU สูงนอกเทรดหลัก ตัวอย่างกรณีการใช้งาน ได้แก่ การจัดเรียงรายการและการแยกวิเคราะห์ JSON
จากตัวอย่างก่อนหน้า คุณสามารถใช้ Dispatcher เพื่อกำหนดฟังก์ชัน
get ใหม่ได้ ภายในเนื้อหาของ get ให้เรียกใช้ withContext(Dispatchers.IO) เพื่อ
สร้างบล็อกที่ทำงานในพูลเธรด I/O โค้ดที่คุณใส่ไว้ในบล็อกนั้นจะทำงานผ่านตัวส่งIOเสมอ เนื่องจาก withContext เป็นฟังก์ชันระงับ ฟังก์ชัน get จึงเป็นฟังก์ชันระงับด้วย
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main
โดย Coroutine ช่วยให้คุณส่งเธรดด้วยการควบคุมแบบละเอียดได้ เนื่องจาก
withContext() ช่วยให้คุณควบคุม Thread Pool ของโค้ดบรรทัดใดก็ได้โดยไม่ต้อง
ใช้ Callback คุณจึงใช้กับฟังก์ชันขนาดเล็กมากได้ เช่น การอ่าน
จากฐานข้อมูลหรือการส่งคำขอเครือข่าย แนวทางปฏิบัติที่ดีคือการใช้
withContext() เพื่อให้แน่ใจว่าทุกฟังก์ชันเป็น main-safe ซึ่งหมายความว่าคุณ
เรียกใช้ฟังก์ชันจากเทรดหลักได้ ด้วยวิธีนี้ ผู้โทรจึงไม่จำเป็นต้อง
คิดว่าควรใช้เธรดใดเพื่อเรียกใช้ฟังก์ชัน
ในตัวอย่างก่อนหน้า fetchDocs() จะดำเนินการในเธรดหลัก แต่สามารถเรียกใช้ get ได้อย่างปลอดภัย ซึ่งจะส่งคำขอเครือข่ายในเบื้องหลัง
เนื่องจากโครูทีนรองรับ suspend และ resume โครูทีนในเทรดหลักจะกลับมาทำงานต่อพร้อมผลลัพธ์ get ทันทีที่บล็อก withContext เสร็จสิ้น
ประสิทธิภาพของ withContext()
withContext()
ไม่ได้เพิ่มค่าใช้จ่ายเพิ่มเติมเมื่อเทียบกับการติดตั้งใช้งานที่เทียบเท่าซึ่งอิงตามการเรียกกลับ
นอกจากนี้ คุณยังเพิ่มประสิทธิภาพwithContext()การโทร
ให้ดีกว่าการติดตั้งใช้งานที่ใช้การเรียกกลับที่เทียบเท่าได้ในบางสถานการณ์ ตัวอย่างเช่น หากฟังก์ชันโทรไปยังเครือข่าย 10 ครั้ง คุณสามารถบอก Kotlin ให้
เปลี่ยนเธรดเพียงครั้งเดียวได้โดยใช้ withContext() ด้านนอก จากนั้นแม้ว่าไลบรารีเครือข่ายจะใช้ withContext() หลายครั้ง แต่ก็จะยังคงอยู่ใน
Dispatcher เดียวกันและหลีกเลี่ยงการสลับเธรด นอกจากนี้ Kotlin ยังเพิ่มประสิทธิภาพการสลับ
ระหว่าง Dispatchers.Default กับ Dispatchers.IO เพื่อหลีกเลี่ยงการสลับเธรด
ทุกครั้งที่ทำได้
เริ่มโครูทีน
คุณเริ่มโครูทีนได้ 2 วิธีดังนี้
launchเริ่มโครูทีนใหม่และไม่ส่งคืนผลลัพธ์ไปยังผู้เรียก คุณสามารถเริ่มงานใดก็ตามที่ถือว่าเป็น "ยิงแล้วทิ้ง" ได้โดยใช้launchasyncจะเริ่มโครูทีนใหม่และช่วยให้คุณส่งคืนผลลัพธ์ด้วยฟังก์ชันระงับ ที่ชื่อawait
โดยปกติแล้ว คุณควรlaunchโครูทีนใหม่จากฟังก์ชันปกติ
เนื่องจากฟังก์ชันปกติเรียกใช้ await ไม่ได้ ใช้ async เฉพาะเมื่ออยู่ภายใน
โครูทีนอื่นหรือเมื่ออยู่ภายในฟังก์ชันระงับและทำการ
แยกย่อยแบบขนาน
การแยกย่อยแบบคู่ขนาน
Coroutine ทั้งหมดที่เริ่มต้นภายในฟังก์ชัน suspend จะต้องหยุดเมื่อฟังก์ชันนั้นแสดงผล ดังนั้นคุณจึงอาจต้องตรวจสอบว่า Coroutine เหล่านั้นเสร็จสิ้นก่อนที่จะแสดงผล การทำงานพร้อมกันแบบมีโครงสร้างใน Kotlin ช่วยให้คุณกำหนด
coroutineScope ที่เริ่มคอร์รูทีนอย่างน้อย 1 รายการได้ จากนั้นใช้ await()
(สำหรับโครูทีนเดียว) หรือ awaitAll() (สำหรับโครูทีนหลายรายการ) คุณจะ
รับประกันได้ว่าโครูทีนเหล่านี้จะทำงานเสร็จก่อนที่จะกลับจากฟังก์ชัน
ตัวอย่างเช่น มากำหนด coroutineScope ที่ดึงข้อมูลเอกสาร 2 รายการแบบไม่พร้อมกันกัน การเรียกใช้ await() ในการอ้างอิงที่เลื่อนออกไปแต่ละรายการจะช่วยให้มั่นใจได้ว่าการดำเนินการ async ทั้ง 2 รายการจะเสร็จสมบูรณ์ก่อนที่จะแสดงผลค่า
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
นอกจากนี้ คุณยังใช้ awaitAll() ในคอลเล็กชันได้ด้วย ดังตัวอย่างต่อไปนี้
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) coroutineScope { val deferreds = listOf( // fetch two docs at the same time async { fetchDoc(1) }, // async returns a result for the first doc async { fetchDoc(2) } // async returns a result for the second doc ) deferreds.awaitAll() // use awaitAll to wait for both network requests }
แม้ว่า fetchTwoDocs() จะเปิดตัวโครูทีนใหม่ด้วย async แต่ฟังก์ชัน
ใช้ awaitAll() เพื่อรอให้โครูทีนที่เปิดตัวเหล่านั้นเสร็จสิ้นก่อน
ที่จะส่งคืน อย่างไรก็ตาม โปรดทราบว่าแม้เราจะไม่ได้เรียกใช้ awaitAll() แต่ตัวสร้าง coroutineScope จะไม่กลับมาทำงานต่อในโครูทีนที่เรียกใช้ fetchTwoDocs จนกว่าโครูทีนใหม่ทั้งหมดจะทำงานเสร็จ
นอกจากนี้ coroutineScope ยังดักจับข้อยกเว้นใดๆ ที่โครูทีนส่ง
และกำหนดเส้นทางกลับไปยังผู้เรียกใช้
ดูข้อมูลเพิ่มเติมเกี่ยวกับการแยกย่อยแบบขนานได้ที่ การเขียนฟังก์ชันระงับ
แนวคิดของโครูทีน
CoroutineScope
CoroutineScope
จะติดตามโครูทีนที่สร้างโดยใช้ launch หรือ async คุณยกเลิก
งานที่กำลังดำเนินการอยู่ (เช่น โครูทีนที่กำลังทำงาน) ได้โดยเรียกใช้
scope.cancel() ได้ทุกเมื่อ ใน Android ไลบรารี KTX บางรายการมีCoroutineScopeของตัวเอง
สำหรับคลาสวงจรบางคลาส เช่น ViewModel มี
viewModelScope
และ Lifecycle มี lifecycleScope
อย่างไรก็ตาม CoroutineScope ไม่ได้เรียกใช้โครูทีนเหมือนกับ Dispatcher
viewModelScope ยังใช้ในตัวอย่างที่อยู่ใน
การทำงานแบบมัลติเธรดในเบื้องหลังบน Android ด้วย Coroutines ด้วย
อย่างไรก็ตาม หากต้องการสร้าง CoroutineScope ของคุณเองเพื่อควบคุมวงจรของ
โครูทีนในเลเยอร์ใดเลเยอร์หนึ่งของแอป คุณสามารถสร้างได้
ดังนี้
class ExampleClass { // Job and Dispatcher are combined into a CoroutineContext which // will be discussed shortly val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine within the scope scope.launch { // New coroutine that can call suspend functions fetchDocs() } } fun cleanUp() { // Cancel the scope to cancel ongoing coroutines work scope.cancel() } }
ขอบเขตที่ยกเลิกแล้วจะสร้างโครูทีนเพิ่มเติมไม่ได้ ดังนั้น คุณควรเรียกใช้
scope.cancel() เฉพาะเมื่อมีการทำลายคลาสที่ควบคุมวงจรของคลาส
เมื่อใช้ viewModelScope คลาส ViewModel จะยกเลิกขอบเขตโดยอัตโนมัติให้คุณในเมธอด onCleared() ของ ViewModel
งาน
Job
คือแฮนเดิลของโครูทีน โครูทีนแต่ละรายการที่คุณสร้างด้วย launch
หรือ async จะแสดงผลอินสแตนซ์ Job ที่ระบุโครูทีนโดยไม่ซ้ำกันและจัดการวงจรของโครูทีน นอกจากนี้ คุณยังส่ง Job ไปยัง
CoroutineScope เพื่อจัดการวงจรของออบเจ็กต์เพิ่มเติมได้ด้วย ดังที่แสดงในตัวอย่างต่อไปนี้
class ExampleClass { // ... fun exampleMethod() { // Handle to the coroutine, you can control its lifecycle val job = scope.launch { // New coroutine } if (condition) { // Cancel the coroutine started above, this doesn't affect the scope // this coroutine was launched in job.cancel() } } }
CoroutineContext
CoroutineContext
กำหนดลักษณะการทำงานของโครูทีนโดยใช้ชุดองค์ประกอบต่อไปนี้
Job: ควบคุมวงจรของโครูทีนCoroutineDispatcher: ส่งงานไปยังเธรดที่เหมาะสมCoroutineName: ชื่อของโครูทีน ซึ่งมีประโยชน์สำหรับการแก้ไขข้อบกพร่องCoroutineExceptionHandler: จัดการข้อยกเว้นที่ไม่ได้แคช
สำหรับโครูทีนใหม่ที่สร้างขึ้นภายในขอบเขต ระบบจะกำหนดอินสแตนซ์ Job ใหม่ให้กับโครูทีนใหม่ และองค์ประกอบ CoroutineContext อื่นๆ
จะรับค่ามาจากขอบเขตที่ครอบคลุม คุณลบล้างองค์ประกอบที่รับค่ามาได้โดยส่ง CoroutineContext ใหม่ไปยังฟังก์ชัน launch หรือ async
โปรดทราบว่าการส่ง Job ไปยัง launch หรือ async จะไม่มีผล
เนื่องจากระบบจะกำหนดอินสแตนซ์ใหม่ของ Job ให้กับโครูทีนใหม่เสมอ
class ExampleClass { val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine on Dispatchers.Main as it's the scope's default val job1 = scope.launch { // New coroutine with CoroutineName = "coroutine" (default) } // Starts a new coroutine on Dispatchers.Default val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) { // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden) } } }
แหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีน
ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้ที่ลิงก์ต่อไปนี้