Bab penutup section Coroutines ini merangkum aturan praktis yang harus selalu diingat saat menulis kode coroutine. Aturan-aturan ini bukan sekadar gaya penulisan — mereka mencegah bug sulit yang umum muncul di production: job yang bocor, thread yang terblokir, pembatalan yang tidak berjalan, hingga race condition. Kita akan melihat setiap aturan disertai contoh kode konkret yang dapat diverifikasi.
Aturan 1: Pakai Structured Concurrency #
Structured concurrency berarti: setiap coroutine harus terikat ke sebuah CoroutineScope dengan lifetime yang jelas. Jangan pakai GlobalScope di kode produksi — job yang berjalan di GlobalScope tidak ikut dibatalkan saat pemanggilnya berhenti, sehingga resource bisa bocor.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicInteger
// ===== ATURAN 1: Structured concurrency =====
// JANGAN: pakai GlobalScope — job bocor ke luar pemanggil
// BENAR: pakai scope eksplisit (coroutineScope, scope injeksi)
class Repository(private val scope: CoroutineScope) {
// suspend function: tidak perlu scope eksplisit — pakai coroutineScope internal
suspend fun ambilSebagian(ids: List<Int>): List<String> = coroutineScope {
ids.map { async { ambilSatu(it) } }.awaitAll()
}
private suspend fun ambilSatu(id: Int): String {
delay(30)
return "item-$id"
}
// Mengembalikan Flow untuk stream: lazy, dibatalkan saat collect berhenti
fun streamData(): Flow<String> = flow {
repeat(3) { i ->
delay(20)
emit("nilai-$i")
}
}.flowOn(Dispatchers.IO)
}
// ===== ATURAN 2: withContext untuk pindah dispatcher =====
class LayananIO {
suspend fun bacaFile(): String = withContext(Dispatchers.IO) {
delay(40) // simulasi baca file
"isi-dari-disk"
}
suspend fun hitungBerat(): Int = withContext(Dispatchers.Default) {
var total = 0
repeat(1_000) { total += it }
total
}
}
// ===== ATURAN 3: Cancellation-aware code =====
suspend fun prosesDenganResource(path: String): String {
var lineDibaca = 0
try {
delay(30) // simulasi operasi async
lineDibaca = 42
return "diproses-$path-$lineDibaca"
} catch (e: CancellationException) {
println("[proses] dibatalkan; baris dibaca = $lineDibaca")
throw e // WAJIB lempar ulang
}
}
// ===== ATURAN 4: Nonce atomik untuk pengujian konkurensi =====
// AtomicInteger aman dipakai dari beberapa thread tanpa sinkronisasi manual
val counter = AtomicInteger(0)
fun main() = runBlocking {
println("=== 1. Structured concurrency: repository.ambilSebagian ===")
val repo = Repository(this)
val hasil = repo.ambilSebagian(listOf(1, 2, 3))
println("hasil = $hasil")
assert(hasil == listOf("item-1", "item-2", "item-3"))
println("assert lolos: structured concurrency mengembalikan urutan sama")
println()
println("=== 2. withContext: pindah dispatcher untuk IO dan CPU ===")
val io = LayananIO()
val (file, total) = io.bacaFile() to io.hitungBerat()
println("file = $file, total = $total")
assert(file == "isi-dari-disk")
assert(total == 499500) // 0+1+2+...+999
println("assert lolos: withContext mengganti dispatcher dan mengembalikan hasil")
println()
println("=== 3. Cancellation-aware: try/catch + lempar ulang ===")
val job = launch {
try {
prosesDenganResource("file-A")
delay(1000) // jika tidak dibatalkan, akan jalan lama
} catch (e: CancellationException) {
println("[main-launch] dibatalkan dengan rapi: ${e::class.simpleName}")
}
}
delay(15)
job.cancelAndJoin()
println("assert lolos: prosesDenganResource menangani pembatalan")
println()
println("=== 4. Flow untuk stream: lazy dan dibatalkan saat collect berhenti ===")
val repoFlow = Repository(this)
val tangkapan = mutableListOf<String>()
val flowJob = launch {
repoFlow.streamData().collect { tangkapan.add(it) }
}
delay(50) // biarkan beberapa emit terjadi
flowJob.cancel()
println("tangkapan (sebelum cancel) = $tangkapan")
assert(tangkapan.size >= 1 && tangkapan.size <= 3)
println("assert lolos: Flow dibatalkan saat job collector dibatalkan")
println()
println("=== 5. AtomicInteger: konkurensi aman tanpa race condition ===")
counter.set(0)
val jobList = (1..10).map {
launch(Dispatchers.Default) {
repeat(100) {
counter.incrementAndGet()
}
}
}
jobList.joinAll()
println("counter akhir = ${counter.get()} (10 * 100 = 1000)")
assert(counter.get() == 1000)
println("assert lolos: AtomicInteger menjaga konsistensi konkurensi")
println()
println("SEMUA PRAKTIK BAIK LOLOS VERIFIKASI")
}Output yang Dihasilkan:
=== 1. Structured concurrency: repository.ambilSebagian ===
hasil = [item-1, item-2, item-3]
assert lolos: structured concurrency mengembalikan urutan sama
=== 2. withContext: pindah dispatcher untuk IO dan CPU ===
file = isi-dari-disk, total = 499500
assert lolos: withContext mengganti dispatcher dan mengembalikan hasil
=== 3. Cancellation-aware: try/catch + lempar ulang ===
[proses] dibatalkan; baris dibaca = 0
[main-launch] dibatalkan dengan rapi: JobCancellationException
assert lolos: prosesDenganResource menangani pembatalan
=== 4. Flow untuk stream: lazy dan dibatalkan saat collect berhenti ===
tangkapan (sebelum cancel) = [nilai-0]
assert lolos: Flow dibatalkan saat job collector dibatalkan
=== 5. AtomicInteger: konkurensi aman tanpa race condition ===
counter akhir = 1000 (10 * 100 = 1000)
assert lolos: AtomicInteger menjaga konsistensi konkurensi
SEMUA PRAKTIK BAIK LOLOS VERIFIKASIMari kita bedah aturan-aturan di atas:
- Aturan 1 (Structured concurrency):
RepositorymenerimaCoroutineScopesebagai parameter — pattern scope injection.ambilSebagianadalahsuspend functionyang menggunakancoroutineScope { }internal, sehingga tidak butuh scope eksplisit.awaitAllmempertahankan urutan hasil. Job yang dibuat di dalamcoroutineScopeotomatis dibatalkan saat scope berhenti. - Aturan 2 (
withContextuntuk ganti dispatcher):bacaFile()memakaiwithContext(Dispatchers.IO)agar operasi blocking tidak mengganggu thread pemanggil.hitungBerat()memakaiDispatchers.Defaultuntuk komputasi CPU. Pola ini lebih disukai daripadalaunch(Dispatchers.IO) { }karenawithContextmengembalikan nilai langsung dan menghormati structured concurrency. - Aturan 3 (Cancellation-aware):
prosesDenganResourcemenangkapCancellationException, melakukan cleanup (mencetak baris yang sudah dibaca), lalu melempar ulang. Aturan krusial: jangan menelanCancellationException— structured concurrency bergantung pada propagasi exception ini untuk membatalkan job dengan benar. - Aturan 4 (Flow untuk stream):
streamData()mengembalikanFlow<String>— bukanJobatauDeferred<List<String>>. Flow bersifat lazy: tidak ada komputasi sampai di-collect. Saat collector dibatalkan, upstream Flow otomatis berhenti. Output Tes 4 menunjukkan tangkapan berisi[nilai-0]— hanya satu emit sempat diproses sebelumcancel(). - Aturan 5 (Konkurensi aman):
AtomicIntegerdigunakan untuk berbagi counter antar coroutine. 10 coroutine × 100 increment = 1000. Tanpa atomic (mis. pakaivar counter: Intbiasa), race condition akan menyebabkan hasil kurang dari 1000.
Aturan Tambahan yang Sering Dilupakan #
Beberapa aturan praktis lain yang patut selalu diingat:
| Aturan | Konsekuensi bila dilanggar |
|---|---|
Buat fungsi suspend, bukan kembalikan Job | Pemanggil tidak bisa await; job bocor |
Jangan blokir thread dengan Thread.sleep di suspend function | Thread dispatcher terkunci; konkurensi rusak |
Gunakan flowOn untuk Flow yang melakukan I/O | Thread pemanggil (mis. UI) terblokir |
Hindari Dispatchers.Unconfined di produksi | Perilaku tidak terduga saat ada delay atau yield |
Tangkap exception di CoroutineExceptionHandler atau try/catch di sekitar await | Exception bocor ke thread uncaught handler |
Pakai SupervisorJob hanya saat child tidak saling membatalkan | Satu child gagal → semua child lain dibatalkan |
Pola: Fungsi Suspend, Bukan Mengembalikan Job #
// Pemanggil tidak tahu kapan selesai, atau harus join manual
fun ambilData(id: Int): Job = scope.launch { ... }// Pemanggil menunggu secara alami; hasil dikembalikan langsung
suspend fun ambilData(id: Int): String {
delay(30)
return "data-$id"
}Pola suspend function lebih idiomatik karena pemanggil bisa try/catch, menggabungkan beberapa pemanggilan dengan async, dan tidak perlu khawatir job bocor. Job sebagai nilai kembalian hanya cocok untuk fire-and-forget yang sangat spesifik (mis. UI toast).
Pola: Berbagi State dengan Channel atau StateFlow #
Untuk berbagi state yang sering berubah antar coroutine, jangan pakai var + Mutex secara manual — gunakan StateFlow:
class CounterViewModel {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value = _count.value + 1 // aman dari race condition
}
}StateFlow menjamin atomicity untuk single-write, dan otomatis memancarkan nilai terbaru ke semua collector.
Referensi Lanjutan #
- Kotlin coroutines overview — praktik terbaik dan structured concurrency
- Coroutine context and dispatchers — panduan memilih dispatcher dan mengelola konteks
Penutup Section Coroutines #
Section ini telah membawa kita dari konsep dasar asynchronous programming hingga praktik terbaik penulisan coroutine. Kita memahami builder (launch, async, runBlocking), structured concurrency, dispatcher, exception handling, hingga Flow dan StateFlow. Bab-bab selanjutnya di Course Kotlin akan beralih ke topik baru: section File I/O dimulai dengan file fundamentals — bagaimana Kotlin memanfaatkan java.io.File dan java.nio.file.Path untuk operasi file yang ringkas dan aman.