runBlocking — Coroutine Builder ใน Kotlin ที่บล็อกเธรดปัจจุบันจนกว่า coroutine ที่ส่งผ่านจะเสร็จสมบูรณ์ แตกต่างจาก launch และ async มันไม่ใช่ฟังก์ชัน suspend และสามารถเรียกใช้จากโค้ดปกติ (บล็อกกิง) ตามเอกสาร JetBrains, 2024 runBlocking ทำหน้าที่เป็นสะพานเชื่อมระหว่างโลกซิงโครนัสและอะซิงโครนัส ทำให้สามารถเริ่ม coroutine จากฟังก์ชัน main และการทดสอบ
ประเด็นสำคัญ
runBlocking เป็นฟังก์ชัน Kotlin ที่สร้าง CoroutineScope ใหม่และเรียกใช้ coroutine ที่ส่งผ่าน โดยบล็อกเธรดปัจจุบันจนกว่าจะเสร็จสมบูรณ์ แตกต่างจาก Coroutine Builder อื่นๆ ทั้งหมด runBlocking ไม่ใช่ฟังก์ชัน suspend และสามารถเรียกใช้จากโค้ดซิงโครนัสปกติ ลายเซ็นของ runBlocking รับ CoroutineContext และบล็อก suspend และส่งกลับผลลัพธ์ชนิด T
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
runBlocking เริ่ม event-loop ใหม่บนเธรดปัจจุบัน เมื่อ coroutine เรียกฟังก์ชัน suspend (เช่น delay() หรือ await()) runBlocking จะบล็อกเธรดและเรียกใช้ coroutine อื่นที่กำหนดไว้บนเธรดเดียวกันจนกว่า coroutine ที่ถูกระงับจะกลับมาทำงานต่อ นี่คือ การบล็อกแบบร่วมมือ — เธรดไม่ว่างเฉย แต่ประมวลผล coroutine อื่น
กลไกภายในของ runBlocking ขึ้นอยู่กับ event-loop: เมื่อเรียกฟังก์ชัน suspend runBlocking จะหยุดการทำงานของบล็อกปัจจุบันชั่วคราวและเรียกใช้ coroutine อื่นจากคิว เมื่อฟังก์ชัน suspend เสร็จสมบูรณ์ การทำงานจะกลับมาดำเนินต่อ วงจรนี้จะดำเนินต่อไปจนกว่า coroutine ทั้งหมดจะสิ้นสุด
runBlocking ใช้พูลเธรดเดียวของตัวเองในการเรียกใช้ coroutine แตกต่างจาก Dispatchers.IO หรือ Default runBlocking ไม่เปลี่ยนเธรด — มันประมวลผล coroutine ทั้งหมดบนเธรดปัจจุบัน โดยสลับการทำงานของพวกมัน นี่คือ บิลเดอร์เดียว ที่รับประกันการทำงานบนเธรดเดียวกัน
fun main() {
val threadName = Thread.currentThread().getName()
println("ก่อน runBlocking บน $threadName")
val result = runBlocking {
println("ภายใน runBlocking บน ${Thread.currentThread().getName()}")
delay(500L)
"Done"
}
println("หลัง runBlocking: $result")
}
ผลลัพธ์จะแสดงว่าคำสั่ง println ทั้งสามคำสั่งทำงานบนเธรดเดียวกัน runBlocking ไม่เปลี่ยนเธรด แต่จัดระเบียบมัลติทาสก์แบบร่วมมือภายในเธรดเดียวโดยใช้ event-loop
runBlocking มีเหตุผลในสามสถานการณ์: จุดเข้า main() ในแอปพลิเคชันคอนโซล การทดสอบหน่วยของฟังก์ชัน suspend และสะพาน — การเรียกโค้ด suspend จากไลบรารีที่ใช้ callback หรือบล็อกกิง ในโค้ด Android โปรดักชัน การใช้บนเธรดหลักเป็นสิ่ง ต้องห้ามโดยเด็ดขาด
| สถานการณ์ | การใช้งานได้ | ความเสี่ยง |
|---|---|---|
| main() ของแอปพลิเคชันคอนโซล | ได้ | ไม่มี — เป็นจุดเข้า เธรดไม่บล็อก UI |
| การทดสอบ JUnit | ได้ | น้อยที่สุด — การทดสอบเป็นซิงโครนัสตามนิยาม |
| เธรด UI Android | ไม่ได้ | ANR, แล็ก, อินเทอร์เฟซค้าง |
| Callback → Coroutine | ได้ ด้วยความระมัดระวัง | การบล็อกพูลเธรดกับการดำเนินการนาน |
สำหรับการทดสอบ Android ให้ใช้ kotlinx-coroutines-test กับ TestDispatcher แทน runBlocking สิ่งนี้ให้การควบคุมเวลา การทำความสะอาดอัตโนมัติ และการแยกการทดสอบ
ในสถานการณ์ส่วนใหญ่ runBlocking สามารถและควรเปลี่ยนด้วยทางเลือกอะซิงโครนัส สำหรับ Android คือ viewModelScope, lifecycleScope หรือ CoroutineScope กับ dispatcher ที่เหมาะสม สำหรับการทดสอบ — TestCoroutineDispatcher และ runTest
// ไม่ดี: runBlocking บนเธรดหลัก Android
runBlocking(Dispatchers.Main) {
val result = networkApi.fetchData()
textView.setText(result)
}
// ดี: lifecycleScope
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { networkApi.fetchData() }
textView.setText(result)
}
สำหรับการทดสอบ ทางเลือกคือ runTest จากไลบรารี kotlinx-coroutines-test มันสร้าง TestCoroutineScope พร้อมเวลาเสมือน ทำให้สามารถทดสอบความล่าช้าโดยไม่ต้องรอจริง สิ่งนี้ช่วยเร่งการทดสอบและทำให้เป็นเชิงกำหนด
สถานการณ์ที่พบบ่อยที่สุดคือการทดสอบฟังก์ชัน suspend runBlocking ในการทดสอบช่วยให้รอผลลัพธ์ของ coroutine แบบซิงโครนัสโดยไม่เปลี่ยนสถาปัตยกรรม สถานการณ์ที่สองคือไลบรารีที่มี API callback ซึ่งฟังก์ชัน suspend ถูกเรียกจากบริบทบล็อกกิงผ่าน runBlocking
// ทดสอบฟังก์ชัน suspend ด้วย runBlocking
class RepositoryTest {
@Test
fun `fetchUser returns correct data`() {
val repository = UserRepository(FakeApi())
val result = runBlocking {
repository.fetchUser("123")
}
assertEquals("John", result.name)
assertEquals("john@test.com", result.email)
}
}
สำหรับสะพานระหว่างโลก callback และ suspend ให้ใช้ CompletableDeferred ร่วมกับ runBlocking แทน callbacks — สิ่งนี้ทำให้ห่วงโซ่การดำเนินการอะซิงโครนัสง่ายขึ้นและปรับปรุงความสามารถในการอ่านโค้ด
การใช้ runBlocking อย่างไม่เหมาะสมเป็นหนึ่งในข้อผิดพลาดทั่วไปเมื่อเปลี่ยนจากวิธีการบล็อกกิงไปยัง coroutine ปัญหาหลัก: การเรียกบนเธรดหลัก Android การซ้อน runBlocking การใช้ภายในฟังก์ชันอะซิงโครนัส และการเริ่มดำเนินการนานผ่าน runBlocking
กฎทอง: runBlocking คือ สะพาน ไม่ใช่สิ่งทดแทน ใช้มันเพียงเพื่อเชื่อมต่อโลกบล็อกกิงและไม่บล็อกกิง สำหรับงานอื่นๆ ทั้งหมด ให้ใช้ launch, async หรือ lifecycleScope
คำถามที่พบบ่อย
runBlocking เป็นบิลเดอร์เดียวที่ไม่ใช่ฟังก์ชัน suspend มันเริ่ม event-loop บนเธรดปัจจุบันและไม่คืนการควบคุมจนกว่า coroutine ทั้งหมดจะเสร็จสมบูรณ์ launch และ async คืนการควบคุมทันที โดยเรียกใช้ coroutine ในพื้นหลัง
ไม่แนะนำ ViewModel มี viewModelScope ในตัวที่จัดการ coroutine โดยอัตโนมัติและยกเลิกเมื่อถูกทำลาย runBlocking ใน ViewBlocking บล็อกเธรดและไม่ตอบสนองต่อการยกเลิกวงจรชีวิต
ใช้ runTest จากไลบรารี kotlinx-coroutines-test มันให้ TestCoroutineScope พร้อมการควบคุมเวลาเสมือน การยกเลิกอัตโนมัติ และการทำงานเชิงกำหนด
Event-loop คือวงจรประมวลผลเหตุการณ์ภายใน runBlocking เมื่อ coroutine ถูกระงับ (เช่น delay()) event-loop จะสลับไปเรียกใช้ coroutine อื่นที่พร้อมบนเธรดเดียวกัน สิ่งนี้สร้างภาพลวงตาของมัลติทาสก์โดยไม่ต้องเปลี่ยนเธรด
runBlocking ที่ซ้อนกัน บนเธรดเดียวกันสร้าง deadlock — บล็อกภายนอกรอบล็อกภายใน แต่ภายในไม่สามารถเริ่มได้จนกว่าภายนอกจะเสร็จสิ้น บนเธรดต่างกันอนุญาตแต่ไม่แนะนำอย่างยิ่งเนื่องจากความซับซ้อนในการดีบัก
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม