Pengantar Coroutine di Kotlin Playground

1. Sebelum memulai

Codelab ini memperkenalkan konkurensi, yang merupakan keterampilan penting yang perlu dipahami oleh developer Android untuk memberikan pengalaman pengguna terbaik. Konkurensi merupakan proses menjalankan beberapa tugas di aplikasi secara bersamaan. Misalnya, aplikasi Anda bisa mendapatkan data dari server web atau menyimpan data pengguna di perangkat, selagi merespons peristiwa input pengguna dan mengupdate UI yang sesuai.

Untuk melakukan tugas secara serentak di aplikasi, Anda akan menggunakan coroutine Kotlin. Coroutine memungkinkan eksekusi blok kode yang ditangguhkan lalu dilanjutkan nanti, sehingga tugas lainnya dapat dilakukan pada saat penangguhan tersebut. Coroutine memudahkan penulisan kode asinkron, yang berarti satu tugas tidak perlu selesai sepenuhnya sebelum memulai tugas berikutnya, sehingga memungkinkan beberapa tugas berjalan serentak.

Codelab ini akan memandu Anda memahami beberapa contoh dasar di Kotlin Playground, tempat Anda berlatih langsung menggunakan coroutine agar lebih nyaman dengan pemrograman asinkron.

Prasyarat

  • Mampu membuat program Kotlin dasar dengan fungsi main()
  • Pengetahuan tentang dasar-dasar bahasa Kotlin, termasuk fungsi dan lambda

Yang akan Anda build

  • Program Kotlin singkat untuk belajar dan bereksperimen dengan dasar-dasar coroutine

Yang akan Anda pelajari

  • Cara coroutine Kotlin dapat menyederhanakan pemrograman asinkron
  • Tujuan dan pentingnya konkurensi terstruktur

Yang akan Anda butuhkan

2. Kode sinkron

Program Sederhana

Dalam kode sinkron, hanya satu tugas konseptual yang sedang berlangsung dalam satu waktu. Anda dapat menganggapnya sebagai jalur linear berurutan. Satu tugas harus selesai sepenuhnya sebelum tugas berikutnya dimulai. Berikut adalah contoh kode sinkron.

  1. Buka Kotlin Playground.
  2. Ganti kode dengan kode berikut untuk program yang menampilkan perkiraan cuaca cerah. Dalam fungsi main(), kita terlebih dulu akan mencetak teks: Weather forecast. Lalu, kita akan mencetak: Sunny.
fun main() {
    println("Weather forecast")
    println("Sunny")
}
  1. Jalankan kode. Output dari menjalankan kode di atas akan menjadi:
Weather forecast
Sunny

println() adalah panggilan sinkron karena tugas mencetak teks ke output selesai sebelum eksekusi dapat dipindahkan ke baris kode berikutnya. Karena setiap panggilan fungsi di main() bersifat sinkron, seluruh fungsi main() akan sinkron. Fungsi bersifat sinkron atau asinkron ditentukan oleh bagian-bagian yang menyusunnya.

Fungsi sinkron hanya ditampilkan saat tugasnya selesai sepenuhnya. Jadi, setelah pernyataan cetak terakhir di main() dieksekusi, semua tugas sudah selesai. Fungsi main() akan ditampilkan dan program akan berakhir.

Menambahkan penundaan

Sekarang, anggaplah semua perkiraan cuaca yang cerah memerlukan permintaan jaringan ke server web jarak jauh. Simulasikan permintaan jaringan dengan menambahkan penundaan dalam kode sebelum mencetak bahwa perkiraan cuaca cerah.

  1. Pertama, tambahkan import kotlinx.coroutines.* di bagian atas kode Anda sebelum fungsi main(). Tindakan ini akan mengimpor fungsi yang akan Anda gunakan dari library coroutine Kotlin.
  2. Ubah kode untuk menambahkan panggilan ke delay(1000), yang menunda eksekusi sisa fungsi main() sebesar 1000 milidetik, atau 1 detik. Sisipkan panggilan delay() ini sebelum pernyataan cetak untuk Sunny.
import kotlinx.coroutines.*

fun main() {
    println("Weather forecast")
    delay(1000)
    println("Sunny")
}

delay() sebenarnya adalah fungsi penangguhan khusus yang disediakan oleh library coroutine Kotlin. Eksekusi fungsi main() akan ditangguhkan (atau dijeda) pada tahap ini, lalu dilanjutkan setelah durasi penundaan yang ditentukan berakhir (dalam kasus ini satu detik).

Jika Anda mencoba menjalankan program pada tahap ini, akan terjadi error kompilasi: Suspend function 'delay' should be called only from a coroutine or another suspend function.

Untuk mempelajari coroutine dalam Kotlin Playground, Anda dapat menggabungkan kode yang ada dengan panggilan ke fungsi runBlocking() dari library coroutine. runBlocking() menjalankan loop peristiwa, yang dapat menangani beberapa tugas sekaligus dengan melanjutkan setiap tugas dari posisi terakhir saat tugas siap dilanjutkan.

  1. Pindahkan konten fungsi main() yang ada ke dalam isi panggilan runBlocking {}. Isi runBlocking{} dieksekusi dalam coroutine baru.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        delay(1000)
        println("Sunny")
    }
}

runBlocking() berjalan secara sinkron; kode ini tidak akan ditampilkan hingga semua tugas dalam blok lambda-nya selesai. Artinya, tugas tersebut akan menunggu tugas dalam panggilan delay() selesai (hingga satu detik berlalu), lalu melanjutkan dengan mengeksekusi pernyataan cetak Sunny. Setelah semua tugas di fungsi runBlocking() selesai, fungsi tersebut akan ditampilkan, yang mengakhiri program.

  1. Jalankan program. Berikut output-nya:
Weather forecast
Sunny

Outputnya sama seperti sebelumnya. Kode tersebut masih sinkron - berjalan dalam garis lurus dan hanya melakukan satu hal dalam satu waktu. Namun, perbedaannya sekarang terletak pada jangka waktu yang lebih lama karena adanya penundaan.

"co-" dalam coroutine berarti kerja sama. Kode ini bekerja sama untuk membagikan loop peristiwa yang mendasarinya saat ditangguhkan untuk menunggu sesuatu, yang memungkinkan tugas lain dijalankan pada saat yang sama. (Bagian "-routine" dalam "coroutine" berarti kumpulan petunjuk seperti fungsi.) Dalam kasus yang ada di contoh ini, coroutine akan ditangguhkan saat mencapai panggilan delay(). Tugas lain dapat dilakukan dalam satu detik tersebut saat coroutine ditangguhkan (meskipun dalam program ini, tidak ada tugas lain yang harus dilakukan). Setelah durasi penundaan berlalu, coroutine akan melanjutkan eksekusi dan dapat melanjutkan pencetakan Sunny ke output.

Fungsi penangguhan

Jika logika yang sebenarnya dalam melakukan permintaan jaringan untuk mendapatkan data cuaca menjadi lebih kompleks, Anda mungkin ingin mengekstrak logika tersebut ke dalam fungsinya sendiri. Mari kita faktorkan ulang kode untuk melihat efeknya.

  1. Ekstrak kode yang menyimulasikan permintaan jaringan untuk data cuaca dan pindahkan ke fungsinya sendiri yang disebut printForecast(). Panggil printForecast() dari kode runBlocking().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

fun printForecast() {
    delay(1000)
    println("Sunny")
}

Jika menjalankan program sekarang, Anda akan melihat error kompilasi yang sama dengan yang terlihat sebelumnya. Fungsi penangguhan hanya dapat dipanggil dari coroutine atau fungsi penangguhan lainnya, jadi tentukan printForecast() sebagai fungsi suspend.

  1. Tambahkan pengubah suspend tepat sebelum kata kunci fun di deklarasi fungsi printForecast() untuk menjadikannya sebagai fungsi penangguhan.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

Ingat bahwa delay() adalah fungsi penangguhan, dan sekarang Anda juga telah membuat printForecast() sebagai fungsi penangguhan.

Fungsi penangguhan mirip dengan fungsi biasa, tetapi dapat ditangguhkan dan dilanjutkan lagi nanti. Untuk melakukannya, fungsi penangguhan hanya dapat dipanggil dari fungsi penangguhan lainnya yang menyediakan kemampuan ini.

Fungsi penangguhan dapat berisi nol atau beberapa titik penangguhan. Titik penangguhan adalah tempat dalam fungsi yang digunakan menangguhkan eksekusi fungsi. Setelah dilanjutkan, eksekusi akan melanjutkan proses terakhir dalam kode dan melanjutkan fungsi lainnya.

  1. Berlatihlah dengan menambahkan fungsi penangguhan lainnya ke kode Anda, di bawah deklarasi fungsi printForecast(). Panggil fungsi penangguhan baru ini printTemperature(). Anda dapat berpura-pura melakukan permintaan jaringan untuk mendapatkan data suhu perkiraan cuaca.

Di dalam fungsi, tunda juga eksekusi sebesar 1000 milidetik, lalu cetak nilai suhu ke output, seperti 30 derajat Celsius. Anda dapat menggunakan urutan escape "\u00b0" untuk mencetak simbol derajat, °.

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Panggil fungsi printTemperature() baru dari kode runBlocking() dalam fungsi main(). Kode lengkapnya sebagai berikut:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
        printTemperature()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
} 
  1. Jalankan program. Output harus berupa:
Weather forecast
Sunny
30°C

Dalam kode ini, coroutine terlebih dahulu ditangguhkan dengan penundaan dalam fungsi penangguhan printForecast(), lalu dilanjutkan setelah penundaan satu detik tersebut. Teks Sunny dicetak ke output. Fungsi printForecast() kembali ke pemanggil.

Selanjutnya, fungsi printTemperature() akan dipanggil. Coroutine tersebut ditangguhkan saat mencapai panggilan delay(), lalu dilanjutkan satu detik kemudian dan selesai mencetak nilai suhu ke output. Fungsi printTemperature() telah menyelesaikan semua tugas dan pengembalian.

Dalam isi runBlocking(), tidak ada tugas lebih lanjut untuk dieksekusi sehingga fungsi runBlocking() akan ditampilkan, dan program berakhir.

Seperti yang disebutkan sebelumnya, runBlocking() bersifat sinkron dan setiap panggilan dalam isi akan dipanggil secara berurutan. Perhatikan bahwa fungsi penangguhan yang dirancang dengan baik hanya akan kembali setelah semua tugas selesai. Akibatnya, fungsi yang ditangguhkan ini berjalan satu demi satu.

  1. (Opsional) Jika ingin melihat waktu yang diperlukan untuk menjalankan program ini dengan penundaan, Anda dapat menggabungkan kode dalam panggilan ke measureTimeMillis() yang akan menampilkan waktu yang dibutuhkan dalam milidetik yang diperlukan untuk menjalankan blok kode yang dikirimkan. Tambahkan pernyataan impor (import kotlin.system.*) agar memiliki akses ke fungsi ini. Cetak waktu eksekusi dan bagi menurut 1000.0 untuk mengonversi milidetik ke detik.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            printForecast()
            printTemperature()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
} 

Output:

Weather forecast
Sunny
30°C
Execution time: 2.128 seconds

Output menunjukkan bahwa perlu waktu ~ 2,1 detik untuk dieksekusi. (Waktu eksekusi yang tepat bisa sedikit berbeda untuk Anda.) Hal tersebut tampaknya wajar karena setiap fungsi penangguhan memiliki penundaan satu detik.

Sejauh ini, Anda telah melihat bahwa kode dalam coroutine dipanggil secara berurutan secara default. Anda harus bersikap eksplisit jika ingin menjalankan semuanya secara serentak, dan Anda akan mempelajari cara melakukannya di bagian berikutnya. Anda akan memanfaatkan loop peristiwa kerja sama untuk melakukan beberapa tugas sekaligus, sehingga akan mempercepat waktu eksekusi program.

3. Kode asinkron

launch()

Gunakan fungsi launch() dari library coroutine untuk meluncurkan coroutine baru. Untuk menjalankan tugas secara serentak, tambahkan beberapa fungsi launch() ke kode Anda agar beberapa coroutine dapat diproses secara bersamaan.

Coroutine di Kotlin mengikuti konsep utama yang disebut konkurensi terstruktur, dengan kode yang berurutan secara default dan bekerja sama dengan loop peristiwa yang mendasarinya, kecuali jika Anda secara eksplisit meminta eksekusi serentak (mis. menggunakan launch()). Asumsinya adalah jika Anda memanggil fungsi, fungsi tersebut akan menyelesaikan tugasnya sepenuhnya pada saat fungsi tersebut ditampilkan, terlepas dari berapa banyak coroutine yang mungkin telah digunakan dalam detail implementasinya. Meskipun gagal dengan pengecualian, setelah pengecualian itu ditampilkan, tidak ada lagi tugas yang tertunda dari fungsi. Oleh karena itu, semua tugas selesai setelah alur kontrol ditampilkan dari fungsi, entah itu menampilkan pengecualian atau menyelesaikan tugasnya dengan sukses.

  1. Mulai dengan kode Anda dari langkah sebelumnya. Gunakan fungsi launch() untuk memindahkan setiap panggilan ke printForecast() dan setiap printTemperature() ke coroutine masing-masing.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
} 
  1. Jalankan program. Berikut output-nya:
Weather forecast
Sunny
30°C

Output-nya sama, tetapi Anda mungkin melihat bahwa lebih cepat menjalankan program. Sebelumnya, Anda harus menunggu fungsi penangguhan printForecast() untuk selesai sepenuhnya sebelum beralih ke fungsi printTemperature(). Sekarang printForecast() dan printTemperature() dapat berjalan secara serentak karena keduanya berada di coroutine terpisah.

Pernyataan println (Perkiraan Cuaca) berada dalam kotak di bagian atas diagram. Di bawahnya, ada panah vertikal yang menunjuk lurus ke bawah. Di luar panah vertikal tersebut, ada cabang yang mengarah ke kanan dengan panah yang menunjuk ke kotak berisi pernyataan printForecast(). Dari panah vertikal sebelumnya juga ada cabang lain yang mengarah ke kanan dengan panah yang menunjuk ke kotak berisi pernyataan printTemperature().

Panggilan ke launch { printForecast() } dapat ditampilkan sebelum semua tugas di printForecast() selesai. Itulah seninya coroutine. Anda dapat melanjutkan ke panggilan launch() berikutnya untuk memulai coroutine berikutnya. Demikian pula, launch { printTemperature() } juga akan ditampilkan bahkan sebelum semua tugas selesai.

  1. (Opsional) Jika ingin melihat seberapa cepat program ini sekarang, Anda dapat menambahkan kode measureTimeMillis() untuk memeriksa waktu eksekusi.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            launch {
                printForecast()
            }
            launch {
                printTemperature()
            }
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}

...

Output:

Weather forecast
Sunny
30°C
Execution time: 1.122 seconds

Anda dapat melihat bahwa waktu eksekusi telah turun dari ~ 2,1 detik menjadi ~ 1,1 detik, jadi lebih cepat untuk menjalankan program setelah Anda menambahkan operasi serentak! Anda dapat menghapus kode pengukuran waktu ini sebelum melanjutkan ke langkah berikutnya.

Apa yang menurut Anda akan terjadi jika Anda menambahkan pernyataan cetak lain setelah panggilan launch() kedua, sebelum akhir kode runBlocking()? Di mana pesan tersebut akan muncul dalam output?

  1. Ubah kode runBlocking() untuk menambahkan pernyataan cetak tambahan sebelum akhir blok tersebut.
...

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
        println("Have a good day!")
    }
}

...
  1. Jalankan program dan berikut output-nya:
Weather forecast
Have a good day!
Sunny
30°C

Dari output ini, Anda dapat mengamati bahwa setelah dua coroutine baru diluncurkan untuk printForecast() dan printTemperature(), Anda dapat melanjutkan dengan petunjuk berikutnya yang mencetak Have a good day!. Hal ini menunjukkan sifat "aktifkan dan lupakan" launch(). Anda mengaktifkan coroutine baru dengan launch(), dan tidak perlu khawatir saat tugasnya selesai.

Nantinya, coroutine akan menyelesaikan tugasnya dan mencetak pernyataan output yang tersisa. Setelah semua tugas (termasuk semua coroutine) dalam isi panggilan runBlocking() selesai, runBlocking() akan ditampilkan dan program akan berakhir.

Sekarang Anda telah mengubah kode sinkron menjadi kode asinkron. Saat fungsi asinkron kembali, tugas mungkin belum selesai. Berikut adalah kasus yang Anda lihat dalam kasus launch(). Fungsi dikembalikan, namun tugasnya belum selesai. Melalui penggunaan launch(), beberapa tugas dapat berjalan secara serentak dalam kode Anda, yang merupakan kemampuan canggih untuk digunakan dalam aplikasi Android yang Anda kembangkan.

async()

Di dunia nyata, Anda tidak akan tahu berapa lama permintaan jaringan yang diperlukan oleh perkiraan cuaca dan suhu. Jika Anda ingin menampilkan laporan cuaca terpadu saat kedua tugas selesai, pendekatan saat ini dengan launch() saja tidak cukup. Di situlah async() berperan.

Gunakan fungsi async() dari library coroutine jika Anda ingin mengetahui kapan coroutine selesai dan memerlukan nilai yang ditampilkan dari coroutine tersebut.

Fungsi async() menampilkan objek berjenis Deferred, yang seperti jaminan bahwa hasilnya akan ada di sana jika sudah siap. Anda dapat mengakses hasilnya pada objek Deferred menggunakan await().

  1. Pertama-tama, ubah fungsi penangguhan untuk menampilkan String, bukan mencetak data perkiraan dan suhu. Perbarui nama fungsi dari printForecast() dan printTemperature() menjadi getForecast() dan getTemperature().
...

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Ubah kode runBlocking() Anda agar menggunakan async(), bukan launch(), untuk kedua coroutine. Simpan nilai return setiap panggilan async() dalam variabel yang disebut forecast dan temperature, yang merupakan objek Deferred yang menyimpan hasil dari jenis String. (Menentukan jenis bersifat opsional karena inferensi jenis di Kotlin, tetapi disertakan di bawah sehingga Anda dapat melihat dengan lebih jelas apa yang ditampilkan oleh panggilan async().)
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        ...
    }
}

...
  1. Nanti di coroutine, setelah dua panggilan async(), Anda dapat mengakses hasil coroutine tersebut dengan memanggil await() pada objek Deferred. Dalam hal ini, Anda dapat mencetak nilai setiap coroutine menggunakan forecast.await() dan temperature.await().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Jalankan program dan output-nya akan seperti berikut:
Weather forecast
Sunny 30°C
Have a good day!

Keren! Anda membuat dua coroutine yang berjalan serentak untuk mendapatkan data perkiraan dan suhu. Setelah setiap proses selesai, nilai akan ditampilkan. Kemudian, Anda menggabungkan kedua nilai yang ditampilkan ke dalam satu pernyataan cetak: Sunny 30°C.

Dekomposisi Paralel

Kita dapat mengembangkan contoh cuaca ini secara lebih detail dan melihat kegunaan coroutine dalam dekomposisi tugas secara paralel. Dekomposisi paralel melibatkan pengambilan masalah dan memecahnya menjadi subtugas yang lebih kecil yang dapat diselesaikan secara paralel. Setelah hasil subtugas siap, Anda dapat menggabungkannya menjadi hasil akhir.

Dalam kode Anda, ekstrak logika laporan cuaca dari isi runBlocking() menjadi fungsi getWeatherReport() tunggal yang menampilkan kombinasi string Sunny 30°C.

  1. Tentukan fungsi penangguhan getWeatherReport() yang baru dalam kode Anda.
  2. Tetapkan fungsi yang sama dengan hasil panggilan ke fungsi coroutineScope{} dengan blok lambda kosong yang nantinya akan berisi logika untuk mendapatkan laporan cuaca.
...

suspend fun getWeatherReport() = coroutineScope {
    
}

...

coroutineScope{} membuat cakupan lokal untuk tugas laporan cuaca ini. Coroutine yang diluncurkan dalam cakupan ini dikelompokkan bersama dalam cakupan ini, yang memiliki implikasi pembatalan dan pengecualian yang akan segera Anda pelajari.

  1. Dalam isi coroutineScope(), buat dua coroutine baru menggunakan async() untuk mengambil data perkiraan dan suhu masing-masing. Buat string laporan cuaca dengan menggabungkan hasil dari kedua coroutine. Lakukan hal ini dengan memanggil await() pada setiap objek Deferred yang ditampilkan oleh panggilan async(). Hal ini memastikan bahwa setiap coroutine menyelesaikan tugasnya dan menampilkan hasilnya, sebelum kita kembali dari fungsi ini.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

...
  1. Panggil fungsi getWeatherReport() baru ini dari runBlocking(). Kode lengkapnya sebagai berikut:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Jalankan program dan Anda akan melihat output ini:
Weather forecast
Sunny 30°C
Have a good day!

Output-nya sama, tetapi ada beberapa poin penting yang perlu diperhatikan di sini. Seperti yang disebutkan sebelumnya, coroutineScope() hanya akan ditampilkan setelah semua tugasnya, termasuk coroutine yang diluncurkan, telah selesai. Dalam hal ini, coroutine getForecast() dan getTemperature() perlu menyelesaikan dan menampilkan hasil masing-masing. Kemudian, teks Sunny dan 30°C digabungkan dan ditampilkan dari cakupan. Laporan cuaca Sunny 30°C ini akan dicetak ke output, dan pemanggil dapat melanjutkan ke pernyataan cetak terakhir Have a good day!.

Dengan coroutineScope(), meskipun fungsi tersebut melakukan tugas internal secara serentak, fungsi ini akan muncul bagi pemanggil sebagai operasi sinkron karena coroutineScope tidak akan ditampilkan hingga semua tugas selesai.

Insight utama untuk konkurensi terstruktur ini adalah bahwa Anda dapat melakukan beberapa operasi serentak dan memasukkannya ke dalam satu operasi sinkron dengan konkurensi berupa detail implementasi. Satu-satunya persyaratan pada kode panggilan adalah berada dalam fungsi atau coroutine penangguhan. Selain itu, struktur kode panggilan tidak perlu memperhitungkan detail konkurensi.

4. Pengecualian dan pembatalan

Sekarang, mari kita bahas beberapa situasi yang dapat menyebabkan error atau beberapa tugas dibatalkan.

Pengantar pengecualian

Pengecualian adalah peristiwa tidak terduga yang terjadi selama eksekusi kode Anda. Anda harus menerapkan cara yang tepat untuk menangani pengecualian ini, guna mencegah aplikasi Anda error dan memengaruhi pengalaman pengguna secara negatif.

Berikut ini contoh program yang dihentikan lebih awal dengan pengecualian. Program ini dimaksudkan untuk menghitung jumlah pizza yang dapat dimakan setiap orang, dengan membagi numberOfPizzas / numberOfPeople. Misalnya, Anda lupa menetapkan nilai numberOfPeople ke nilai sebenarnya.

fun main() {
    val numberOfPeople = 0
    val numberOfPizzas = 20
    println("Slices per person: ${numberOfPizzas / numberOfPeople}")
}

Saat Anda menjalankan program, program akan mengalami error dengan pengecualian aritmetika karena Anda tidak dapat membagi angka dengan nol.

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at FileKt.main (File.kt:4)
 at FileKt.main (File.kt:-1)
 at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (:-2)

Masalah ini memiliki perbaikan langsung, yaitu Anda dapat mengubah nilai awal numberOfPeople menjadi angka bukan nol. Namun, saat kode Anda menjadi lebih kompleks, ada kasus tertentu yang membuat Anda tidak dapat mengantisipasi dan mencegah semua pengecualian terjadi.

Apa yang terjadi jika salah satu coroutine Anda gagal dengan pengecualian? Ubah kode dari program cuaca untuk mencari tahu.

Pengecualian dengan coroutine

  1. Mulai dengan program cuaca dari bagian sebelumnya.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

Dalam salah satu fungsi penangguhan, tampilkan pengecualian secara sengaja untuk melihat efeknya. Tindakan ini menyimulasikan terjadinya error tidak terduga saat mengambil data dari server, yang mungkin saja terjadi.

  1. Dalam fungsi getTemperature(), tambahkan baris kode yang menampilkan pengecualian. Tulis ekspresi tampilan menggunakan kata kunci throw di Kotlin, diikuti dengan instance baru pengecualian yang diperluas dari Throwable.

Misalnya, Anda dapat menampilkan AssertionError dan meneruskan string pesan yang menjelaskan error secara lebih mendetail: throw AssertionError("Temperature is invalid"). Menampilkan pengecualian ini akan menghentikan eksekusi fungsi getTemperature() lebih lanjut.

...

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}

Anda juga dapat mengubah penundaan menjadi 500 milidetik untuk metode getTemperature(), sehingga Anda tahu pengecualian akan terjadi sebelum fungsi getForecast() lainnya dapat menyelesaikan tugasnya.

  1. Jalankan program untuk melihat hasilnya.
Weather forecast
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
 at FileKt.getTemperature (File.kt:24)
 at FileKt$getTemperature$1.invokeSuspend (File.kt:-1)
 at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)

Untuk memahami perilaku ini, Anda perlu mengetahui bahwa ada hubungan induk-turunan di antara coroutine. Anda dapat meluncurkan coroutine (dikenal sebagai turunan) dari coroutine lain (induk). Saat meluncurkan lebih banyak coroutine dari coroutine tersebut, Anda dapat membuat seluruh hierarki coroutine.

Coroutine yang menjalankan getTemperature() dan coroutine yang menjalankan getForecast() adalah coroutine turunan dari coroutine induk yang sama. Perilaku yang Anda lihat dengan pengecualian dalam coroutine disebabkan oleh konkurensi terstruktur. Jika salah satu coroutine turunan gagal dengan pengecualian, coroutine tersebut akan disebarkan ke atas. Coroutine induk dibatalkan sehingga membatalkan coroutine turunan lainnya (misalnya, coroutine yang menjalankan getForecast() dalam kasus ini). Terakhir, error menyebar ke atas dan program mengalami error dengan AssertionError.

Pengecualian try-catch

Jika mengetahui bahwa bagian tertentu dari kode Anda mungkin dapat memunculkan pengecualian, Anda dapat mengapit kode tersebut dengan blok try-catch. Anda dapat menangkap pengecualian dan menanganinya dengan lebih baik di aplikasi, misalnya dengan menampilkan pesan error yang berguna kepada pengguna. Berikut adalah cuplikan kode tampilannya:

try {
    // Some code that may throw an exception
} catch (e: IllegalArgumentException) {
    // Handle exception
}

Pendekatan ini juga berfungsi untuk kode asinkron dengan coroutine. Anda masih dapat menggunakan ekspresi try-catch untuk menangkap dan menangani pengecualian dalam coroutine. Alasannya karena dengan konkurensi terstruktur, kode berurutan masih berupa kode sinkron sehingga blok try-catch akan tetap berfungsi dengan cara yang sama.

...

fun main() {
    runBlocking {
        ...
        try {
            ...
            throw IllegalArgumentException("No city selected")
            ...
        } catch (e: IllegalArgumentException) {
            println("Caught exception $e")
            // Handle error
        }
    }
}

...

Agar lebih nyaman dalam menangani pengecualian, ubah program cuaca untuk menangkap pengecualian yang Anda tambahkan sebelumnya, lalu cetak pengecualian ke output.

  1. Dalam fungsi runBlocking(), tambahkan blok try-catch di sekitar kode yang memanggil getWeatherReport(). Cetak error yang tertangkap dan cetak juga pesan bahwa laporan cuaca tidak tersedia.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        try {
            println(getWeatherReport())
        } catch (e: AssertionError) {
            println("Caught exception in runBlocking(): $e")
            println("Report unavailable at this time")
        }
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Jalankan program, dan sekarang error telah ditangani dengan baik, dan program berhasil dijalankan.
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!

Dari output, Anda dapat mengamati bahwa getTemperature() menampilkan pengecualian. Dalam isi fungsi runBlocking(), Anda akan mengelilingi panggilan println(getWeatherReport()) dalam blok try-catch. Anda akan menangkap jenis pengecualian yang diharapkan (AssertionError dalam kasus contoh ini). Kemudian, Anda akan mencetak pengecualian ke output sebagai "Caught exception", diikuti dengan string pesan error. Untuk menangani error tersebut, Anda akan memberi tahu pengguna bahwa laporan cuaca tidak tersedia dengan pernyataan println() tambahan: Report unavailable at this time.

Perhatikan bahwa perilaku ini berarti bahwa jika terjadi kegagalan dalam mendapatkan suhu, tidak akan ada laporan cuaca sama sekali (meskipun perkiraan yang valid diambil).

Tergantung pada bagaimana Anda ingin program berperilaku, ada cara alternatif untuk menangani pengecualian dalam program cuaca.

  1. Pindahkan penanganan error agar perilaku try-catch benar-benar terjadi dalam coroutine yang diluncurkan oleh async() untuk mengambil suhu. Dengan demikian, laporan cuaca tetap dapat mencetak perkiraan, meskipun suhu gagal. Berikut kodenya:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Jalankan program.
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!

Dari output, Anda dapat melihat bahwa memanggil getTemperature() gagal dengan pengecualian, tetapi kode dalam async() dapat menangkap pengecualian tersebut dan menanganinya dengan baik dengan menggunakan coroutine yang masih menampilkan String yang menyatakan suhu tidak ditemukan. Laporan cuaca masih dapat dicetak, dengan perkiraan keberhasilan sebesar Sunny. Suhu di laporan cuaca tidak ada, tetapi muncul pesan yang menjelaskan bahwa suhu tidak ditemukan. Ini adalah pengalaman pengguna yang lebih baik daripada program mengalami error.

Pemahaman sederhana terkait pendekatan penanganan error ini adalah bahwa async() merupakan produsen saat coroutine dimulai dengannya. await() adalah konsumen karena menunggu untuk memakai hasil dari coroutine. Produsen melakukan tugas itu dan memberikan hasil. Konsumen memakai hasilnya. Jika ada pengecualian di produsen, konsumen akan mendapatkan pengecualian tersebut jika tidak ditangani, dan coroutine akan gagal. Namun, jika produsen dapat menangkap dan menangani pengecualian, maka konsumen tidak akan melihat pengecualian tersebut dan akan melihat hasil yang valid.

Berikut ini kode getWeatherReport() lagi untuk referensi:

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

Dalam hal ini, produsen (async()) dapat menangkap dan menangani pengecualian, tetapi tetap menampilkan hasil String dari "{ No temperature found }". Konsumen (await()) menerima hasil String ini dan bahkan tidak perlu mengetahui bahwa telah terjadi pengecualian. Ini adalah opsi lain untuk menangani dengan baik pengecualian yang Anda harapkan dapat terjadi dalam kode Anda.

Sekarang Anda telah mempelajari bahwa pengecualian menyebar ke atas dalam hierarki coroutine, kecuali jika ditangani. Penting juga untuk berhati-hati saat pengecualian menyebar hingga ke root hierarki, yang dapat menyebabkan error di seluruh aplikasi. Pelajari lebih lanjut penanganan pengecualian di artikel postingan blog Pengecualian dalam coroutine dan Penanganan pengecualian coroutine.

Pembatalan

Topik yang serupa dengan pengecualian adalah pembatalan coroutine. Skenario ini biasanya berdasarkan pengguna saat sebuah peristiwa menyebabkan aplikasi membatalkan tugas yang telah dimulai sebelumnya.

Misalnya, pengguna telah memilih preferensi di aplikasi bahwa mereka tidak ingin lagi melihat nilai suhu di aplikasi. Mereka hanya ingin mengetahui perkiraan cuaca (misalnya Sunny), tetapi bukan suhu persisnya. Oleh karena itu, batalkan coroutine yang saat ini mendapatkan data suhu.

  1. Pertama-tama, mulailah dengan kode awal di bawah ini (tanpa pembatalan).
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Setelah beberapa penundaan, batalkan coroutine yang mengambil informasi suhu, sehingga laporan cuaca hanya menampilkan perkiraan. Ubah nilai return blok coroutineScope menjadi string perkiraan cuaca saja.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    
    delay(200)
    temperature.cancel()

    "${forecast.await()}"
}

...
  1. Jalankan program. Outputnya sekarang adalah sebagai berikut. Laporan cuaca hanya terdiri dari perkiraan cuaca Sunny, tetapi tidak dengan suhu karena coroutine tersebut dibatalkan.
Weather forecast
Sunny
Have a good day!

Yang Anda pelajari di sini adalah bahwa coroutine dapat dibatalkan, tetapi tidak akan memengaruhi coroutine lain dalam cakupan yang sama dan coroutine induk tidak akan dibatalkan.

Di bagian ini, Anda telah melihat perilaku pembatalan dan pengecualian dalam coroutine dan kaitannya dengan hierarki coroutine. Mari pelajari lebih lanjut konsep formal di balik coroutine, sehingga Anda dapat memahami bagaimana semua bagian penting digabungkan.

5. Konsep coroutine

Saat menjalankan tugas secara asinkron atau serentak, ada pertanyaan yang perlu Anda jawab tentang bagaimana tugas akan dieksekusi, berapa lama coroutine harus ada, apa yang harus terjadi jika dibatalkan atau gagal dengan sebuah error, dan lainnya. Coroutine mengikuti prinsip konkurensi terstruktur, yang mengharuskan Anda menjawab pertanyaan-pertanyaan ini ketika menggunakan coroutine dalam kode Anda menggunakan kombinasi mekanisme.

Tugas

Saat Anda meluncurkan coroutine dengan fungsi launch(), coroutine akan menampilkan instance Job. Tugas menyimpan handle, atau referensi, ke coroutine, sehingga Anda dapat mengelola siklus prosesnya.

val job = launch { ... }

Tugas ini dapat digunakan untuk mengontrol siklus proses, atau berapa lama coroutine aktif, seperti membatalkan coroutine jika Anda tidak memerlukan tugas lagi.

job.cancel()

Dengan tugas, Anda dapat memeriksa apakah tugas tersebut aktif, dibatalkan, atau telah selesai. Tugas ini akan selesai jika coroutine dan coroutine yang diluncurkannya telah menyelesaikan semua tugasnya. Perhatikan bahwa coroutine mungkin telah selesai karena alasan yang berbeda, seperti dibatalkan, atau gagal dengan pengecualian, tetapi tugas masih dianggap selesai pada saat itu.

Tugas juga melacak hubungan induk-turunan di antara coroutine.

Hierarki tugas

Jika coroutine meluncurkan coroutine lain, tugas yang ditampilkan dari coroutine baru akan disebut turunan dari tugas induk asli.

val job = launch {
    ...            

    val childJob = launch { ... }

    ...
}

Hubungan induk-turunan ini membentuk hierarki tugas, tempat setiap tugas dapat meluncurkan tugas, dan seterusnya.

Diagram ini menunjukkan hierarki struktur tugas. Akar hierarki adalah tugas induk. Tugas ini memiliki 3 turunan yang disebut: Tugas Turunan 1, Tugas Turunan 2, dan Tugas Turunan 3. Kemudian Tugas Turunan 1 memiliki dua turunannya sendiri: Tugas Turunan 1a dan Tugas Turunan 1b. Selain itu, Tugas Turunan 2 memiliki satu turunan bernama Tugas Turunan 2a. Terakhir, Tugas Turunan 3 memiliki dua turunan: Tugas Turunan 3a dan Tugas Turunan 3b.

Hubungan induk-turunan ini penting karena hubungan tersebut akan menentukan perilaku tertentu untuk turunan dan induk, serta turunan lain dari induk yang sama. Anda melihat perilaku ini dalam contoh sebelumnya dengan program cuaca.

  • Jika tugas induk dibatalkan, tugas turunannya juga akan dibatalkan.
  • Jika tugas turunan dibatalkan menggunakan job.cancel(), tugas tersebut akan dihentikan, tetapi tidak membatalkan induknya.
  • Jika tugas gagal dengan pengecualian, tugas akan membatalkan induknya dengan pengecualian tersebut. Hal ini dikenal sebagai penyebaran error ke atas (ke induk, induknya induk, dan seterusnya). .

CoroutineScope

Coroutine biasanya diluncurkan ke CoroutineScope. Hal ini memastikan bahwa kita tidak memiliki coroutine yang tidak dikelola dan hilang, yang dapat membuang-buang resource.

launch() dan async() adalah fungsi ekstensi pada CoroutineScope. Panggil launch() atau async() pada cakupan untuk membuat coroutine baru dalam cakupan tersebut.

CoroutineScope terikat dengan siklus proses, yang menetapkan batas durasi aktif coroutine dalam cakupan tersebut. Jika cakupan dibatalkan, tugasnya akan dibatalkan, dan pembatalan tersebut akan disebarkan ke tugas turunannya. Jika tugas turunan dalam cakupan gagal dengan pengecualian, tugas turunan lainnya akan dibatalkan, tugas induk akan dibatalkan, dan pengecualian akan ditampilkan ulang ke pemanggil.

CoroutineScope di Kotlin Playground

Dalam codelab ini, Anda telah menggunakan runBlocking() yang menyediakan CoroutineScope untuk program Anda. Anda juga mempelajari cara menggunakan coroutineScope { } untuk membuat cakupan baru dalam fungsi getWeatherReport().

CoroutineScope di aplikasi Android

Android memberikan dukungan cakupan coroutine dalam entity yang memiliki siklus proses yang ditentukan dengan baik, seperti Activity (lifecycleScope) dan ViewModel (viewModelScope). Coroutine yang dimulai dalam cakupan ini akan mematuhi siklus proses entity yang sesuai, seperti Activity atau ViewModel.

Misalnya, Anda memulai coroutine dalam Activity dengan cakupan coroutine yang disediakan bernama lifecycleScope. Jika aktivitas dihancurkan, lifecycleScope akan dibatalkan dan semua coroutine turunannya akan otomatis dibatalkan juga. Anda hanya perlu menentukan apakah coroutine yang mengikuti siklus proses Activity adalah perilaku yang Anda inginkan.

Di aplikasi Android Race Tracker yang sedang dikerjakan, Anda akan mempelajari cara menentukan cakupan coroutine ke siklus proses composable.

Detail Implementasi CoroutineScope

Jika memeriksa kode sumber tentang cara menerapkan CoroutineScope.kt di library coroutine Kotlin, Anda dapat melihat bahwa CoroutineScope dideklarasikan sebagai antarmuka dan berisi CoroutineContext sebagai variabel.

Fungsi launch() dan async() membuat coroutine turunan baru dalam cakupan tersebut dan turunan juga mewarisi konteks dari cakupan tersebut. Apa yang terdapat dalam konteks? Mari kita bahas hal ini berikutnya.

CoroutineContext

CoroutineContext memberikan informasi tentang konteks tempat coroutine akan berjalan. Pada dasarnya, CoroutineContext adalah peta yang menyimpan elemen dengan setiap elemen memiliki kunci yang unik. Kolom ini tidak wajib diisi, tetapi berikut beberapa contoh yang dapat dimuat dalam konteks:

  • name - nama coroutine untuk mengidentifikasinya secara unik
  • job - mengontrol siklus proses coroutine
  • dispatcher - mengirimkan tugas ke thread yang sesuai
  • exception handler - menangani pengecualian yang ditampilkan oleh kode yang dieksekusi di coroutine

Setiap elemen dalam konteks dapat ditambahkan beserta operator +. Misalnya, satu CoroutineContext dapat ditentukan sebagai berikut:

Job() + Dispatchers.Main + exceptionHandler

Karena nama tidak diberikan, nama coroutine default akan digunakan.

Dalam coroutine, jika Anda meluncurkan coroutine baru, coroutine turunan akan mewarisi CoroutineContext dari coroutine induk, tetapi menggantikan tugas khusus untuk coroutine yang baru saja dibuat. Anda juga dapat mengganti elemen apa pun yang diwarisi dari konteks induk dengan meneruskan argumen ke fungsi launch() atau async() untuk bagian konteks yang ingin dibedakan.

scope.launch(Dispatchers.Default) {
    ...
}

Anda dapat mempelajari lebih lanjut CoroutineContext dan cara konteksnya diwarisi dari induk dalam video bincang-bincang konferensi KotlinConf ini.

Anda telah melihat sebutan dispatcher beberapa kali. Perannya adalah untuk mengirim atau memberikan tugas ke thread. Mari kita bahas thread dan dispatcher secara lebih mendetail.

Dispatcher

Coroutine menggunakan dispatcher untuk menentukan thread yang akan digunakan untuk eksekusinya. Thread dapat dimulai, melakukan beberapa tugas (mengeksekusi beberapa kode), lalu berakhir saat tidak ada lagi tugas yang harus dilakukan.

Saat pengguna memulai aplikasi, sistem Android akan membuat proses baru dan satu thread eksekusi untuk aplikasi Anda, yang dikenal sebagai thread utama. Thread utama menangani banyak operasi penting untuk aplikasi Anda termasuk peristiwa sistem Android, menggambar UI pada layar, menangani peristiwa input pengguna, dan lainnya. Akibatnya, sebagian besar kode yang Anda tulis untuk aplikasi kemungkinan akan berjalan di thread utama.

Ada dua istilah yang harus dipahami terkait perilaku threading kode Anda: pemblokiran dan non-pemblokiran. Fungsi reguler memblokir thread panggilan hingga tugasnya selesai. Artinya, fungsi tersebut tidak menghasilkan thread panggilan hingga tugasnya selesai, sehingga tidak ada tugas lain yang dapat dilakukan pada saat yang sama. Sebaliknya, kode non-pemblokir akan menghasilkan thread panggilan hingga kondisi tertentu terpenuhi, sehingga Anda dapat melakukan tugas lain pada saat yang sama. Anda dapat menggunakan fungsi asinkron untuk melakukan tugas non-pemblokiran karena fungsi tersebut ditampilkan sebelum tugasnya selesai.

Dalam kasus aplikasi Android, Anda hanya boleh memanggil kode pemblokir di thread utama jika kode tersebut akan dieksekusi dengan cukup cepat. Tujuannya adalah untuk membuat thread utama tidak diblokir, sehingga dapat langsung menjalankan tugas jika peristiwa baru dipicu. Thread utama ini adalah UI thread untuk aktivitas Anda dan bertanggung jawab atas gambar UI dan peristiwa terkait UI. Saat ada perubahan di layar, UI perlu digambar ulang. Untuk konten seperti animasi di layar, UI harus sering digambar ulang agar tampak seperti transisi yang lancar. Jika thread utama perlu menjalankan blok tugas yang berjalan lama, layar tidak akan sering diperbarui dan pengguna akan melihat transisi mendadak (dikenal sebagai "jank") atau aplikasi mungkin berhenti merespons atau lambat merespons.

Oleh karena itu, kita perlu memindahkan item tugas yang berjalan lama dari thread utama dan menanganinya dalam thread yang berbeda. Aplikasi dimulai dengan satu thread utama, tetapi Anda dapat memilih untuk membuat beberapa thread untuk melakukan tugas tambahan. Thread tambahan ini dapat disebut sebagai thread pekerja. Tidak masalah jika tugas yang berjalan lama memblokir thread pekerja untuk waktu yang lama, karena pada saat itu thread utama tidak diblokir dan dapat merespons pengguna secara aktif.

Ada beberapa dispatcher bawaan yang disediakan Kotlin:

  • Dispatchers.Main: Gunakan dispatcher ini untuk menjalankan coroutine pada thread utama Android. Dispatcher ini digunakan terutama untuk menangani pembaruan dan interaksi UI, serta melakukan tugas cepat.
  • Dispatchers.IO: Dispatcher ini dioptimalkan untuk menjalankan disk atau I/O jaringan di luar thread utama. Misalnya, membaca dari atau menulis ke file, dan menjalankan operasi jaringan apa pun.
  • Dispatchers.Default: Ini adalah dispatcher default yang digunakan saat memanggil launch() dan async(), jika tidak ada dispatcher yang ditentukan dalam konteksnya. Anda dapat menggunakan dispatcher ini untuk melakukan tugas yang menggunakan banyak komputasi di luar thread utama. Misalnya, memproses file gambar bitmap.

Coba contoh berikut di Kotlin Playground untuk lebih memahami dispatcher coroutine.

  1. Ganti kode apa pun yang Anda miliki di Kotlin Playground dengan kode berikut:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        launch {
            delay(1000)
            println("10 results found.")
        }
        println("Loading...")
    }
}
  1. Sekarang, gabungkan konten coroutine yang diluncurkan dengan panggilan ke withContext() untuk mengubah CoroutineContext tempat eksekusi coroutine, dan secara khusus mengganti dispatcher. Beralihlah ke Dispatchers.Default (bukan Dispatchers.Main yang saat ini digunakan untuk kode coroutine lainnya dalam program).
...

fun main() {
    runBlocking {
        launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                println("10 results found.")
            }
        }
        println("Loading...")
    }
}

Anda dapat beralih dispatcher karena withContext() merupakan fungsi penangguhan. Mengeksekusi blok kode yang disediakan menggunakan CoroutineContext baru. Konteks baru berasal dari konteks tugas induk (blok launch() luar), kecuali jika konteks ini menggantikan dispatcher yang digunakan dalam konteks induk dengan yang ditentukan di sini: Dispatchers.Default. Inilah yang dapat kita lakukan, mulai dari mengeksekusi tugas dengan Dispatchers.Main menjadi menggunakan Dispatchers.Default.

  1. Jalankan program. Output harus berupa:
Loading...
10 results found.
  1. Tambahkan pernyataan cetak untuk melihat thread yang Anda gunakan dengan memanggil Thread.currentThread().name.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("${Thread.currentThread().name} - runBlocking function")
                launch {
            println("${Thread.currentThread().name} - launch function")
            withContext(Dispatchers.Default) {
                println("${Thread.currentThread().name} - withContext function")
                delay(1000)
                println("10 results found.")
            }
            println("${Thread.currentThread().name} - end of launch function")
        }
        println("Loading...")
    }
}
  1. Jalankan program. Output harus berupa:
main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function

Dari output ini, Anda dapat mengamati bahwa sebagian besar kode dieksekusi di coroutine pada thread utama. Namun, untuk bagian kode Anda dalam blok withContext(Dispatchers.Default) dieksekusi dalam coroutine pada thread pekerja Dispatcher Default (yang bukan thread utama). Perhatikan bahwa setelah withContext() ditampilkan, coroutine akan kembali berjalan di thread utama (dibuktikan dengan pernyataan output: main @coroutine#2 - end of launch function). Contoh ini menunjukkan bahwa Anda dapat berganti dispatcher dengan mengubah konteks yang digunakan untuk coroutine.

Jika Anda memiliki coroutine yang dimulai di thread utama, dan ingin memindahkan operasi tertentu dari thread utama, Anda dapat menggunakan withContext untuk mengalihkan dispatcher yang digunakan untuk tugas tersebut. Pilih dari dispatcher yang tersedia: Main, Default, dan IO, sesuai jenis operasinya. Kemudian tugas tersebut dapat ditetapkan ke thread (atau grup thread yang disebut kumpulan thread) yang ditetapkan untuk tujuan tersebut. Coroutine dapat menangguhkan dirinya sendiri, dan dispatcher juga dapat memengaruhi cara melanjutkan tugas.

Perhatikan bahwa saat menangani library populer seperti Room dan Retrofit (di unit ini dan yang berikutnya), Anda mungkin tidak harus berganti dispatcher secara eksplisit jika kode library sudah menangani tugas ini menggunakan dispatcher coroutine alternatif seperti Dispatchers.IO. Dalam kasus tersebut, fungsi suspend yang ditampilkan oleh library tersebut mungkin sudah berupa main-safe dan dapat dipanggil dari coroutine yang berjalan di thread utama. Library itu sendiri akan menangani pengalihan dispatcher ke dispatcher yang menggunakan thread pekerja.

Sekarang Anda telah mendapatkan ringkasan tingkat tinggi tentang bagian-bagian penting dari coroutine dan peran yang dimainkan CoroutineScope, CoroutineContext, CoroutineDispatcher, dan Jobs dalam membentuk siklus proses dan perilaku coroutine Google.

6. Kesimpulan

Anda berhasil mengerjakan topik coroutine yang menantang ini! Anda telah mempelajari bahwa coroutine sangat berguna karena eksekusinya dapat ditangguhkan, yang mengosongkan thread yang mendasarinya untuk melakukan tugas lain, lalu coroutine dapat dilanjutkan nanti. Hal ini memungkinkan Anda menjalankan operasi kode secara serentak.

Kode coroutine dalam Kotlin mengikuti prinsip konkurensi terstruktur. Kode ini berurutan secara default, jadi Anda harus melakukannya secara eksplisit jika menginginkan konkurensi (misalnya menggunakan launch() atau async()). Dengan konkurensi terstruktur, Anda dapat melakukan beberapa operasi konkurensi dan memasukkannya ke dalam satu operasi sinkron dengan detail implementasi. Satu-satunya persyaratan pada kode panggilan adalah berada dalam fungsi atau coroutine penangguhan. Selain itu, struktur kode panggilan tidak perlu memperhitungkan detail konkurensi. Hal tersebut membuat kode asinkron lebih mudah dibaca dan dipahami.

Konkurensi terstruktur melacak setiap coroutine yang diluncurkan di aplikasi Anda dan memastikan bahwa coroutine tidak hilang. Coroutine dapat memiliki hierarki—tugas dapat meluncurkan subtugas, yang pada akhirnya dapat meluncurkan subtugas. Tugas mempertahankan hubungan induk-turunan di antara coroutine, dan memungkinkan Anda mengontrol siklus proses coroutine.

Peluncuran, penyelesaian, pembatalan, dan kegagalan merupakan empat operasi umum dalam eksekusi coroutine. Untuk mempermudah pengelolaan program serentak, konkurensi terstruktur menentukan prinsip yang membentuk dasar tentang pengelolaan operasi umum dalam hierarki:

  1. Peluncuran: Meluncurkan coroutine ke dalam cakupan yang memiliki batas durasi aktifnya.
  2. Penyelesaian: Tugas akan selesai jika tugas turunannya sudah selesai.
  3. Pembatalan: Operasi ini harus disebarkan ke bawah. Jika coroutine dibatalkan, coroutine turunan juga harus dibatalkan.
  4. Kegagalan: Operasi ini harus menyebar ke atas. Jika coroutine menampilkan pengecualian, induk akan membatalkan semua turunannya, membatalkan dirinya sendiri, dan menyebarkan pengecualian ke induknya. Tindakan ini berlanjut hingga kegagalan terdeteksi dan ditangani. Hal ini akan memastikan bahwa setiap error dalam kode dilaporkan dengan benar dan tidak pernah hilang.

Melalui praktik langsung dengan coroutine dan memahami konsep di balik coroutine, kini Anda lebih siap menulis kode serentak di aplikasi Android. Dengan menggunakan coroutine untuk pemrograman asinkron, kode Anda akan lebih mudah dibaca dan dipahami, lebih kuat dalam situasi pembatalan dan pengecualian, serta memberikan pengalaman yang lebih optimal dan responsif bagi pengguna akhir.

Ringkasan

  • Coroutine memungkinkan Anda menulis kode berdurasi panjang dan berjalan serentak tanpa mempelajari gaya pemrograman baru. Eksekusi coroutine memiliki desain yang berurutan.
  • Coroutine mengikuti prinsip konkurensi terstruktur, yang membantu memastikan bahwa tugas tidak hilang dan terikat ke cakupan dengan batas tertentu durasi aktifnya. Kode Anda berurutan secara default dan bekerja sama dengan loop peristiwa yang mendasarinya, kecuali jika Anda secara eksplisit meminta eksekusi serentak (mis. menggunakan launch() atau async()). Asumsinya adalah jika Anda memanggil fungsi, fungsi tersebut akan menyelesaikan tugasnya sepenuhnya (kecuali jika gagal dengan pengecualian) pada saat fungsi tersebut ditampilkan terlepas dari berapa banyak coroutine yang mungkin telah digunakan dalam detail implementasinya.
  • Pengubah suspend digunakan untuk menandai fungsi yang eksekusinya dapat ditangguhkan dan dilanjutkan di lain waktu.
  • Fungsi suspend hanya dapat dipanggil dari fungsi penangguhan lain atau dari coroutine.
  • Anda dapat memulai coroutine baru menggunakan fungsi ekstensi launch() atau async() pada CoroutineScope.
  • Tugas memainkan peran penting untuk memastikan konkurensi terstruktur dengan mengelola siklus proses coroutine dan mempertahankan hubungan induk-turunan.
  • CoroutineScope mengontrol masa aktif coroutine melalui Tugasnya dan menerapkan pembatalan serta aturan lainnya untuk turunannya dan turunan berikutnya secara berulang.
  • CoroutineContext menentukan perilaku coroutine, dan dapat menyertakan referensi ke tugas dan dispatcher coroutine.
  • Coroutine menggunakan CoroutineDispatcher untuk menentukan thread yang akan digunakan untuk eksekusinya.

Mempelajari lebih lanjut