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
- Akses internet untuk menggunakan Kotlin Playground
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.
- Buka Kotlin Playground.
- 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")
}
- 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.
- Pertama, tambahkan
import kotlinx.coroutines.*
di bagian atas kode Anda sebelum fungsimain()
. Tindakan ini akan mengimpor fungsi yang akan Anda gunakan dari library coroutine Kotlin. - Ubah kode untuk menambahkan panggilan ke
delay(1000)
, yang menunda eksekusi sisa fungsimain()
sebesar1000
milidetik, atau 1 detik. Sisipkan panggilandelay()
ini sebelum pernyataan cetak untukSunny
.
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.
- Pindahkan konten fungsi
main()
yang ada ke dalam isi panggilanrunBlocking {}
. IsirunBlocking{}
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.
- 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.
- Ekstrak kode yang menyimulasikan permintaan jaringan untuk data cuaca dan pindahkan ke fungsinya sendiri yang disebut
printForecast()
. PanggilprintForecast()
dari koderunBlocking()
.
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
.
- Tambahkan pengubah
suspend
tepat sebelum kata kuncifun
di deklarasi fungsiprintForecast()
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.
- Berlatihlah dengan menambahkan fungsi penangguhan lainnya ke kode Anda, di bawah deklarasi fungsi
printForecast()
. Panggil fungsi penangguhan baru iniprintTemperature()
. 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")
}
- Panggil fungsi
printTemperature()
baru dari koderunBlocking()
dalam fungsimain()
. 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")
}
- 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.
- (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 menurut1000.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.
- Mulai dengan kode Anda dari langkah sebelumnya. Gunakan fungsi
launch()
untuk memindahkan setiap panggilan keprintForecast()
dan setiapprintTemperature()
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")
}
- 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.
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.
- (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?
- 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!")
}
}
...
- 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()
.
- Pertama-tama, ubah fungsi penangguhan untuk menampilkan
String
, bukan mencetak data perkiraan dan suhu. Perbarui nama fungsi dariprintForecast()
danprintTemperature()
menjadigetForecast()
dangetTemperature()
.
...
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
- Ubah kode
runBlocking()
Anda agar menggunakanasync()
, bukanlaunch()
, untuk kedua coroutine. Simpan nilai return setiap panggilanasync()
dalam variabel yang disebutforecast
dantemperature
, yang merupakan objekDeferred
yang menyimpan hasil dari jenisString
. (Menentukan jenis bersifat opsional karena inferensi jenis di Kotlin, tetapi disertakan di bawah sehingga Anda dapat melihat dengan lebih jelas apa yang ditampilkan oleh panggilanasync()
.)
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
val forecast: Deferred<String> = async {
getForecast()
}
val temperature: Deferred<String> = async {
getTemperature()
}
...
}
}
...
- Nanti di coroutine, setelah dua panggilan
async()
, Anda dapat mengakses hasil coroutine tersebut dengan memanggilawait()
pada objekDeferred
. Dalam hal ini, Anda dapat mencetak nilai setiap coroutine menggunakanforecast.await()
dantemperature.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"
}
- 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
.
- Tentukan fungsi penangguhan
getWeatherReport()
yang baru dalam kode Anda. - 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.
- Dalam isi
coroutineScope()
, buat dua coroutine baru menggunakanasync()
untuk mengambil data perkiraan dan suhu masing-masing. Buat string laporan cuaca dengan menggabungkan hasil dari kedua coroutine. Lakukan hal ini dengan memanggilawait()
pada setiap objekDeferred
yang ditampilkan oleh panggilanasync()
. 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()}"
}
...
- Panggil fungsi
getWeatherReport()
baru ini darirunBlocking()
. 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"
}
- 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
- 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.
- Dalam fungsi
getTemperature()
, tambahkan baris kode yang menampilkan pengecualian. Tulis ekspresi tampilan menggunakan kata kuncithrow
di Kotlin, diikuti dengan instance baru pengecualian yang diperluas dariThrowable
.
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.
- 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.
- Dalam fungsi
runBlocking()
, tambahkan blok try-catch di sekitar kode yang memanggilgetWeatherReport()
. 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"
}
- 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.
- 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"
}
- 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.
- 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"
}
- 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()}"
}
...
- 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.
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()
danasync()
, 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.
- 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...")
}
}
- Sekarang, gabungkan konten coroutine yang diluncurkan dengan panggilan ke
withContext()
untuk mengubahCoroutineContext
tempat eksekusi coroutine, dan secara khusus mengganti dispatcher. Beralihlah keDispatchers.Default
(bukanDispatchers.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
.
- Jalankan program. Output harus berupa:
Loading... 10 results found.
- 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...")
}
}
- 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:
- Peluncuran: Meluncurkan coroutine ke dalam cakupan yang memiliki batas durasi aktifnya.
- Penyelesaian: Tugas akan selesai jika tugas turunannya sudah selesai.
- Pembatalan: Operasi ini harus disebarkan ke bawah. Jika coroutine dibatalkan, coroutine turunan juga harus dibatalkan.
- 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()
atauasync()
). 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()
atauasync()
padaCoroutineScope
. - 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.