Dispatchers ใน Kotlin Coroutines เป็นส่วนประกอบของ CoroutineContext ที่กำหนดเธรดสำหรับการดำเนินการ coroutines: Main (เธรด UI), IO (เครือข่ายและดิสก์), Default (งานที่ใช้ CPU สูง) และ Unconfined (เธรดปัจจุบัน) ตัวจัดส่งแต่ละตัวจัดการกลุ่มเธรดเฉพาะทางที่ปรับให้เหมาะสมกับงานประเภทใดประเภทหนึ่ง ตามคู่มือ JetBrains, 2024 การเลือกตัวจัดส่งที่ถูกต้องมีความสำคัญต่อประสิทธิภาพและความเสถียรของแอปพลิเคชัน
ประเด็นสำคัญ
Dispatchers คือการนำอินเทอร์เฟซ CoroutineDispatcher ไปใช้ ซึ่งเป็นองค์ประกอบของ CoroutineContext พวกมันกำหนดว่า coroutine จะดำเนินการบนเธรดหรือกลุ่มเธรดใด เมื่อสร้าง coroutine ผ่าน launch หรือ async ตัวจัดส่งสามารถส่งเป็นพารามิเตอร์แรกได้: launch(Dispatchers.IO) { ... } หากไม่ได้ระบุตัวจัดส่งไว้ มันจะสืบทอดจาก CoroutineScope ภายนอก
Kotlin มีตัวจัดส่งในตัวสี่ตัว: Main, IO, Default, Unconfined ตัวจัดส่งแต่ละตัวใช้กลุ่มเธรดของตัวเองที่ปรับให้เหมาะสมกับการดำเนินการประเภทใดประเภทหนึ่งโดยเฉพาะ การเลือกตัวจัดส่งที่ถูกต้อง เป็นตัวกำหนดประสิทธิภาพของแอปพลิเคชัน: การเลือกที่ผิดนำไปสู่การหน่วงของ UI, คอร์ CPU ว่างงาน หรือการใช้เธรดที่ไม่มีประสิทธิภาพ
| ตัวจัดส่ง | กลุ่มเธรด | เธรดสูงสุด | การใช้งาน |
|---|---|---|---|
| Dispatchers.Main | หนึ่ง (UI) | 1 | อัปเดต UI, LiveData, View |
| Dispatchers.IO | กลุ่ม IO | 64 (limitedParallelism) | เครือข่าย, ไฟล์, DB |
| Dispatchers.Default | กลุ่ม CPU | N คอร์ | การเรียงลำดับ, การแยกวิเคราะห์, การคำนวณ |
| Dispatchers.Unconfined | เธรดปัจจุบัน | ไม่มี | การดำเนินการกลาง, การทดสอบ |
Dispatchers.Main คือตัวจัดส่งที่ดำเนินการ coroutines บนเธรดหลักของ Android มันออกแบบมาสำหรับการดำเนินการที่เกี่ยวข้องกับอินเทอร์เฟซ: การอัปเดต TextView, การเรียก notifyDataSetChanged, การทำงานกับ LiveData และ StateFlow ใน Android ตัวจัดส่งนี้ถูกนำไปใช้ผ่าน Handler (Looper.getMainLooper())
// Correct switch to Main for UI updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
หาก coroutine อยู่บนตัวจัดส่ง Main อยู่แล้ว withContext(Dispatchers.Main) เพิ่มเติมจะไม่สร้างโอเวอร์เฮด — ตัวจัดส่งจะตรวจสอบเธรดปัจจุบันและข้ามการสลับ withContext เป็นวิธีที่ต้องการในการสลับระหว่างตัวจัดส่ง
Dispatchers.IO คือตัวจัดส่งที่ปรับให้เหมาะสมสำหรับการดำเนินการ I/O: คำขอ HTTP (Ktor, OkHttp), การอ่านและเขียนไฟล์, การทำงานกับ Room หรือ SQLDelight มันใช้กลุ่ม 64 เธรด โดยค่าเริ่มต้น ปรับขนาดได้ภายใต้โหลด คำขอ I/O ใหม่แต่ละรายการสามารถสร้างเธรดเพิ่มเติมได้จนกว่าจะถึงขีดจำกัด
เพื่อควบคุมจำนวนการดำเนินการ I/O พร้อมกัน ให้ใช้ limitedParallelism() ฟังก์ชันนี้สร้างตัวจัดส่งใหม่ที่มีขีดจำกัดจำนวนเธรดแบบขนาน ป้องกันการหมดของกลุ่มระหว่างการดำเนินการจำนวนมาก
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Load 100 files with limit of 4 concurrent operations
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
ใช้ตัวจัดส่ง IO สำหรับการดำเนินการทั้งหมดที่ coroutine ใช้เวลาในการรอ (I/O-bound) งานที่ใช้ CPU สูงบนตัวจัดส่ง IO ไม่มีประสิทธิภาพ — พวกมันครอบครองเธรดที่ intended สำหรับ I/O ทำให้ ปริมาณงาน ของระบบลดลง
Dispatchers.Default คือตัวจัดส่งสำหรับการดำเนินการคำนวณที่โหลดโปรเซสเซอร์: การเรียงลำดับ, การกรอง, การแยกวิเคราะห์ JSON (Moshi, Kotlinx Serialization), การประมวลผลภาพ, การคำนวณ ขนาดกลุ่มเท่ากับจำนวนคอร์ของโปรเซสเซอร์ (แต่ไม่น้อยกว่า 2) ซึ่งช่วยให้มั่นใจถึงการใช้งาน CPU สูงสุดโดยไม่ต้องสลับบริบท
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
อย่าใช้ Dispatchers.Default สำหรับการดำเนินการ I/O — สิ่งนี้จะบล็อกเธรดกลุ่ม CPU ที่สามารถประมวลผลงานคำนวณได้ การแยก IO และ Default ช่วยให้ ใช้ทรัพยากร ระบบอย่างเหมาะสมที่สุด: เธรด IO รอ I/O, เธรด CPU ยุ่งอยู่กับการคำนวณตลอดเวลา
Dispatchers.Unconfined คือตัวจัดส่งพิเศษที่ไม่ผูก coroutine กับกลุ่มใด ๆ Coroutine เริ่มดำเนินการในเธรดที่เรียก launch/async และหลังจากระงับ จะดำเนินการต่อในเธรดที่เรียก resume พฤติกรรมนี้เหมาะสมสำหรับการดำเนินการกลางที่ไม่ต้องการบริบทคงที่
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${Thread.currentThread().getName()}")
}
}
ในโค้ดการผลิต Dispatchers.Unconfined ถูกใช้น้อยมาก กรณีการใช้งานหลัก: การแปลงแบบเบา ก่อนส่งข้อมูลไปยังตัวจัดส่งอื่นและการทดสอบ สำหรับภาระงานการผลิต ให้ใช้ตัวจัดส่งที่ชัดเจน — Unconfined ไม่สามารถคาดเดาได้เนื่องจากเธรดการดำเนินการขึ้นอยู่กับการใช้งาน resume
การเลือกตัวจัดส่งขึ้นอยู่กับประเภทงาน: การดำเนินการ UI → Main, I/O-bound → IO, CPU-bound → Default, กลาง → สืบทอดจากขอบเขต สำหรับ Android แนะนำให้เริ่ม coroutine บนตัวจัดส่งที่ทำงานหลัก และสลับไปยัง Main ผ่าน withContext ก่อนอัปเดต UI
สำหรับสถานการณ์ที่ซับซ้อน ให้รวมตัวจัดส่งโดยใช้ตัวดำเนินการ +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler สิ่งนี้สร้าง CoroutineContext ด้วยตัวจัดส่งที่ระบุ การจัดการข้อผิดพลาด และลำดับชั้น Job ที่แยกออกจากกัน
คำถามที่พบบ่อย
Dispatchers.IO ใช้กลุ่มสูงสุด 64 เธรดสำหรับการดำเนินการ I/O-bound (รอ I/O) ในขณะที่ Dispatchers.Default ใช้กลุ่มตามจำนวนคอร์ CPU สำหรับงานคำนวณ เมื่อเธรดขาดแคลน ทั้งสองกลุ่มสามารถแชร์เธรดซึ่งกันและกันได้
ได้ ใช้ newSingleThreadContext() สำหรับเธรดเดียวหรือ newFixedThreadPoolContext() สำหรับกลุ่มคงที่ สำหรับการผลิต ให้ใช้ limitedParallelism() บนพื้นฐานของตัวจัดส่งที่มีอยู่ — สิ่งนี้มีประสิทธิภาพมากกว่าการสร้างกลุ่มใหม่
หาก Dispatchers.Main ไม่พร้อมใช้งาน (เช่น ในการทดสอบ JUnit หรือบริการพื้นหลัง) IllegalStateException จะถูกโยน ใช้ TestCoroutineDispatcher สำหรับการทดสอบ และ Dispatchers.IO หรือ Default สำหรับบริการพื้นหลัง
ใช้ Dispatchers.IO.limitedParallelism(N) โดยที่ N คือจำนวนเธรดแบบขนานสูงสุด สิ่งนี้ป้องกันการหมดของกลุ่มระหว่างคำขอจำนวนมากและให้ความขนานที่ควบคุมได้
Dispatchers.Unconfined เหมาะสำหรับการดำเนินการกลาง: การแปลงข้อมูลแบบเบาก่อนส่งไปยังตัวจัดส่งอื่น สถานการณ์การทดสอบ ในโค้ด Android การผลิต ไม่แนะนำเนื่องจากเธรดการดำเนินการไม่แน่นอนหลังจากการระงับ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม